neoagent 3.2.1-beta.1 → 3.2.1-beta.3
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +30803 -30805
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +5 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +358 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +158 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +87 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +84 -9
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const path = require('path');
|
|
2
4
|
const fs = require('fs');
|
|
3
5
|
const { spawn } = require('child_process');
|
|
4
6
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
7
|
+
const { validateCloudUrlWithDns } = require('../../utils/cloud-security');
|
|
8
|
+
const { runProcess } = require('../android/process');
|
|
5
9
|
const {
|
|
6
10
|
chooseBrowserIdentity,
|
|
7
11
|
detectBotChallenge,
|
|
@@ -127,40 +131,66 @@ function resolveBrowserExecutablePath() {
|
|
|
127
131
|
return platformCandidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
function installPlaywrightBrowserBinary(browserName) {
|
|
134
|
+
async function installPlaywrightBrowserBinary(browserName, options = {}) {
|
|
131
135
|
const packageRoot = path.dirname(require.resolve('playwright-chromium/package.json'));
|
|
132
136
|
const cliPath = path.join(packageRoot, 'cli.js');
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let stderr = '';
|
|
137
|
+
await runProcess(process.execPath, [cliPath, 'install', '--no-shell', browserName], {
|
|
138
|
+
signal: options.signal,
|
|
139
|
+
timeoutMs: 10 * 60 * 1000,
|
|
140
|
+
maxOutputBytes: 32 * 1024 * 1024,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
144
|
+
function createAbortError(signal, fallback = 'Browser operation aborted.') {
|
|
145
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
146
|
+
const error = new Error(String(signal?.reason || fallback));
|
|
147
|
+
error.name = 'AbortError';
|
|
148
|
+
error.code = 'ABORT_ERR';
|
|
149
|
+
return error;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function throwIfAborted(signal) {
|
|
153
|
+
if (signal?.aborted) throw createAbortError(signal);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sleep(ms, signal = null) {
|
|
157
|
+
throwIfAborted(signal);
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
signal?.removeEventListener('abort', onAbort);
|
|
161
|
+
resolve();
|
|
162
|
+
}, ms);
|
|
163
|
+
const onAbort = () => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(createAbortError(signal));
|
|
166
|
+
};
|
|
167
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
168
|
+
if (signal?.aborted) onAbort();
|
|
159
169
|
});
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
function
|
|
163
|
-
return
|
|
172
|
+
function isAbortError(error, signal = null) {
|
|
173
|
+
return Boolean(
|
|
174
|
+
signal?.aborted
|
|
175
|
+
|| error?.name === 'AbortError'
|
|
176
|
+
|| error?.code === 'ABORT_ERR',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function raceWithSignal(promise, signal) {
|
|
181
|
+
throwIfAborted(signal);
|
|
182
|
+
if (!signal) return promise;
|
|
183
|
+
let onAbort;
|
|
184
|
+
const aborted = new Promise((_, reject) => {
|
|
185
|
+
onAbort = () => reject(createAbortError(signal));
|
|
186
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
187
|
+
if (signal.aborted) onAbort();
|
|
188
|
+
});
|
|
189
|
+
try {
|
|
190
|
+
return await Promise.race([promise, aborted]);
|
|
191
|
+
} finally {
|
|
192
|
+
signal.removeEventListener('abort', onAbort);
|
|
193
|
+
}
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
async function waitForFile(filePath, options = {}) {
|
|
@@ -171,7 +201,7 @@ async function waitForFile(filePath, options = {}) {
|
|
|
171
201
|
}
|
|
172
202
|
const startedAt = Date.now();
|
|
173
203
|
while (Date.now() - startedAt < timeoutMs) {
|
|
174
|
-
await sleep(intervalMs);
|
|
204
|
+
await sleep(intervalMs, options.signal);
|
|
175
205
|
if (fs.existsSync(filePath)) {
|
|
176
206
|
return true;
|
|
177
207
|
}
|
|
@@ -197,6 +227,17 @@ function normalizeWaitUntil(waitUntil) {
|
|
|
197
227
|
return 'domcontentloaded';
|
|
198
228
|
}
|
|
199
229
|
|
|
230
|
+
function normalizePointCoordinate(value, label) {
|
|
231
|
+
if (value == null || value === '') {
|
|
232
|
+
throw new Error(`${label} coordinate is required.`);
|
|
233
|
+
}
|
|
234
|
+
const normalized = Number(value);
|
|
235
|
+
if (!Number.isFinite(normalized)) {
|
|
236
|
+
throw new Error(`${label} coordinate must be a finite number.`);
|
|
237
|
+
}
|
|
238
|
+
return Math.round(normalized);
|
|
239
|
+
}
|
|
240
|
+
|
|
200
241
|
function clearChromiumSingletonLocks(profileDir) {
|
|
201
242
|
const lockEntries = [
|
|
202
243
|
'SingletonLock',
|
|
@@ -213,6 +254,18 @@ function clearChromiumSingletonLocks(profileDir) {
|
|
|
213
254
|
}
|
|
214
255
|
}
|
|
215
256
|
|
|
257
|
+
function chooseVirtualDisplay() {
|
|
258
|
+
for (let number = 90; number < 200; number += 1) {
|
|
259
|
+
if (
|
|
260
|
+
!fs.existsSync(`/tmp/.X11-unix/X${number}`)
|
|
261
|
+
&& !fs.existsSync(`/tmp/.X${number}-lock`)
|
|
262
|
+
) {
|
|
263
|
+
return `:${number}`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
throw new Error('No free X11 display is available for the browser.');
|
|
267
|
+
}
|
|
268
|
+
|
|
216
269
|
class BrowserController {
|
|
217
270
|
constructor(options = {}) {
|
|
218
271
|
this.io = options.io || null;
|
|
@@ -226,9 +279,15 @@ class BrowserController {
|
|
|
226
279
|
this.page = null;
|
|
227
280
|
this.displayProcess = null;
|
|
228
281
|
this.displayValue = process.env.DISPLAY || null;
|
|
282
|
+
this._managedDisplayValue = null;
|
|
229
283
|
this.launching = false;
|
|
230
284
|
this.launchPromise = null;
|
|
231
285
|
this.browserBinaryInstallPromise = null;
|
|
286
|
+
this._launchAbortController = null;
|
|
287
|
+
this._urlValidator = options.urlValidator || validateCloudUrlWithDns;
|
|
288
|
+
this._boundPages = new WeakSet();
|
|
289
|
+
this._closing = false;
|
|
290
|
+
this._closePromise = null;
|
|
232
291
|
this.headless = false;
|
|
233
292
|
this.profileDir = path.join(BROWSER_PROFILE_ROOT, this.userId || 'default');
|
|
234
293
|
if (!fs.existsSync(this.profileDir)) fs.mkdirSync(this.profileDir, { recursive: true });
|
|
@@ -251,6 +310,122 @@ class BrowserController {
|
|
|
251
310
|
return this.close();
|
|
252
311
|
}
|
|
253
312
|
|
|
313
|
+
_contextIsOpen() {
|
|
314
|
+
if (!this.context) return false;
|
|
315
|
+
if (typeof this.context.isClosed === 'function' && this.context.isClosed()) return false;
|
|
316
|
+
return !this.browser
|
|
317
|
+
|| typeof this.browser.isConnected !== 'function'
|
|
318
|
+
|| this.browser.isConnected();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_clearBrowserReferences(context, browser) {
|
|
322
|
+
if (context && this.context !== context) return;
|
|
323
|
+
if (!context && browser && this.browser !== browser) return;
|
|
324
|
+
this.context = null;
|
|
325
|
+
this.browser = null;
|
|
326
|
+
this.page = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_bindPage(page, options = {}) {
|
|
330
|
+
if (!page) return;
|
|
331
|
+
if (!this._boundPages.has(page)) {
|
|
332
|
+
this._boundPages.add(page);
|
|
333
|
+
const clearPage = () => {
|
|
334
|
+
if (this.page === page) this.page = null;
|
|
335
|
+
};
|
|
336
|
+
page.on?.('close', clearPage);
|
|
337
|
+
page.on?.('crash', clearPage);
|
|
338
|
+
}
|
|
339
|
+
if (options.makeActive !== false) this.page = page;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_bindContextLifecycle(context, browser) {
|
|
343
|
+
context.on?.('close', () => this._clearBrowserReferences(context, browser));
|
|
344
|
+
context.on?.('page', (page) => {
|
|
345
|
+
this._bindPage(page);
|
|
346
|
+
this._applyStealthToPage(page).catch(() => {});
|
|
347
|
+
});
|
|
348
|
+
browser?.on?.('disconnected', () => this._clearBrowserReferences(context, browser));
|
|
349
|
+
for (const page of context.pages?.() || []) this._bindPage(page, { makeActive: false });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async _networkUrlAllowed(url) {
|
|
353
|
+
let parsed;
|
|
354
|
+
try {
|
|
355
|
+
parsed = new URL(String(url || ''));
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
360
|
+
if (protocol === 'about:' && parsed.href === 'about:blank') return true;
|
|
361
|
+
if (protocol === 'blob:' || protocol === 'data:') return true;
|
|
362
|
+
if (protocol === 'ws:' || protocol === 'wss:') {
|
|
363
|
+
parsed.protocol = protocol === 'ws:' ? 'http:' : 'https:';
|
|
364
|
+
} else if (protocol !== 'http:' && protocol !== 'https:') {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
const result = await this._urlValidator(parsed.href);
|
|
368
|
+
return result?.allowed === true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async _assertNavigationAllowed(url, options = {}) {
|
|
372
|
+
let result = null;
|
|
373
|
+
try {
|
|
374
|
+
result = await this._urlValidator(String(url || ''), { signal: options.signal });
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (isAbortError(error, options.signal)) throw error;
|
|
377
|
+
}
|
|
378
|
+
if (result?.allowed !== true) {
|
|
379
|
+
const error = new Error('This URL is not permitted.');
|
|
380
|
+
error.code = 'URL_BLOCKED';
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async _installNetworkGuard(context) {
|
|
386
|
+
await context.route('**/*', async (route) => {
|
|
387
|
+
let allowed = false;
|
|
388
|
+
try {
|
|
389
|
+
allowed = await this._networkUrlAllowed(route.request().url());
|
|
390
|
+
} catch {}
|
|
391
|
+
if (!allowed) {
|
|
392
|
+
await route.abort('blockedbyclient').catch(() => {});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await route.continue().catch(() => {});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (typeof context.routeWebSocket === 'function') {
|
|
399
|
+
await context.routeWebSocket('**', async (webSocket) => {
|
|
400
|
+
let allowed = false;
|
|
401
|
+
try {
|
|
402
|
+
allowed = await this._networkUrlAllowed(webSocket.url());
|
|
403
|
+
} catch {}
|
|
404
|
+
if (!allowed) {
|
|
405
|
+
await webSocket.close({ code: 1008, reason: 'Blocked by network policy' }).catch(() => {});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
webSocket.connectToServer();
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async _withPageCancellation(page, signal, operation) {
|
|
414
|
+
throwIfAborted(signal);
|
|
415
|
+
if (!signal) return operation();
|
|
416
|
+
const closeOnAbort = () => {
|
|
417
|
+
if (!page?.isClosed?.()) {
|
|
418
|
+
page.close({ runBeforeUnload: false, reason: 'Browser operation cancelled' }).catch(() => {});
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
signal.addEventListener('abort', closeOnAbort, { once: true });
|
|
422
|
+
try {
|
|
423
|
+
return await raceWithSignal(Promise.resolve().then(operation), signal);
|
|
424
|
+
} finally {
|
|
425
|
+
signal.removeEventListener('abort', closeOnAbort);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
254
429
|
async _applyStealthToPage(page) {
|
|
255
430
|
const ua = this._userAgent;
|
|
256
431
|
const vp = this._viewport;
|
|
@@ -308,7 +483,6 @@ class BrowserController {
|
|
|
308
483
|
arr.item = i => arr[i];
|
|
309
484
|
arr.namedItem = n => arr.find(p => p.name === n) || null;
|
|
310
485
|
arr.refresh = () => {};
|
|
311
|
-
Object.defineProperty(arr, 'length', { get: () => arr.length });
|
|
312
486
|
return arr;
|
|
313
487
|
}
|
|
314
488
|
});
|
|
@@ -345,17 +519,6 @@ class BrowserController {
|
|
|
345
519
|
return originalFillText.apply(this, args);
|
|
346
520
|
};
|
|
347
521
|
|
|
348
|
-
// Media Devices Spoofing
|
|
349
|
-
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
|
350
|
-
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
|
|
351
|
-
navigator.mediaDevices.enumerateDevices = async () => {
|
|
352
|
-
return [
|
|
353
|
-
{ kind: 'audioinput', deviceId: 'default', groupId: 'a', label: 'MacBook Pro Microphone' },
|
|
354
|
-
{ kind: 'audiooutput', deviceId: 'default', groupId: 'b', label: 'MacBook Pro Speakers' },
|
|
355
|
-
{ kind: 'videoinput', deviceId: 'default', groupId: 'c', label: 'FaceTime HD Camera' }
|
|
356
|
-
];
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
522
|
})();
|
|
360
523
|
`;
|
|
361
524
|
if (typeof page.evaluateOnNewDocument === 'function') {
|
|
@@ -365,23 +528,42 @@ class BrowserController {
|
|
|
365
528
|
}
|
|
366
529
|
}
|
|
367
530
|
|
|
368
|
-
async ensureBrowser() {
|
|
369
|
-
|
|
531
|
+
async ensureBrowser(options = {}) {
|
|
532
|
+
let signal = options.signal;
|
|
533
|
+
throwIfAborted(signal);
|
|
534
|
+
if (this._closePromise) await raceWithSignal(this._closePromise, signal);
|
|
535
|
+
if (this._contextIsOpen()) return;
|
|
370
536
|
if (this.launchPromise) {
|
|
371
|
-
await this.launchPromise;
|
|
537
|
+
await raceWithSignal(this.launchPromise, signal);
|
|
372
538
|
return;
|
|
373
539
|
}
|
|
374
540
|
|
|
541
|
+
const staleContext = this.context;
|
|
542
|
+
const staleBrowser = this.browser;
|
|
543
|
+
this._clearBrowserReferences(staleContext, staleBrowser);
|
|
544
|
+
if (staleContext) await staleContext.close().catch(() => {});
|
|
545
|
+
else if (staleBrowser) await staleBrowser.close().catch(() => {});
|
|
546
|
+
|
|
547
|
+
const launchAbortController = new AbortController();
|
|
548
|
+
this._launchAbortController = launchAbortController;
|
|
549
|
+
const externalSignal = signal;
|
|
550
|
+
const forwardAbort = () => launchAbortController.abort(externalSignal.reason);
|
|
551
|
+
externalSignal?.addEventListener('abort', forwardAbort, { once: true });
|
|
552
|
+
if (externalSignal?.aborted) forwardAbort();
|
|
553
|
+
signal = launchAbortController.signal;
|
|
554
|
+
|
|
375
555
|
this.launching = true;
|
|
376
556
|
this.launchPromise = (async () => {
|
|
377
557
|
const runtimeReady = await waitForFile(BROWSER_READY_MARKER, {
|
|
378
558
|
timeoutMs: 10 * 60 * 1000,
|
|
379
559
|
intervalMs: 1000,
|
|
560
|
+
signal,
|
|
380
561
|
});
|
|
381
562
|
if (!runtimeReady) {
|
|
382
563
|
throw new Error('Browser runtime provisioning is still in progress inside the VM. Retry shortly.');
|
|
383
564
|
}
|
|
384
|
-
await this.ensureVirtualDisplay();
|
|
565
|
+
await this.ensureVirtualDisplay({ signal });
|
|
566
|
+
throwIfAborted(signal);
|
|
385
567
|
|
|
386
568
|
this._identity = chooseBrowserIdentity(this.userId || this.profileDir);
|
|
387
569
|
this._userAgent = this._identity.userAgent;
|
|
@@ -394,10 +576,10 @@ class BrowserController {
|
|
|
394
576
|
let executablePath = resolveBrowserExecutablePath();
|
|
395
577
|
if (!executablePath) {
|
|
396
578
|
if (!this.browserBinaryInstallPromise) {
|
|
397
|
-
this.browserBinaryInstallPromise = installPlaywrightBrowserBinary(this.engine);
|
|
579
|
+
this.browserBinaryInstallPromise = installPlaywrightBrowserBinary(this.engine, { signal });
|
|
398
580
|
}
|
|
399
581
|
try {
|
|
400
|
-
await this.browserBinaryInstallPromise;
|
|
582
|
+
await raceWithSignal(this.browserBinaryInstallPromise, signal);
|
|
401
583
|
} finally {
|
|
402
584
|
this.browserBinaryInstallPromise = null;
|
|
403
585
|
}
|
|
@@ -444,7 +626,7 @@ class BrowserController {
|
|
|
444
626
|
|
|
445
627
|
const playwright = require('playwright-chromium');
|
|
446
628
|
clearChromiumSingletonLocks(this.profileDir);
|
|
447
|
-
|
|
629
|
+
const launchPromise = playwright.chromium.launchPersistentContext(this.profileDir, {
|
|
448
630
|
headless: false,
|
|
449
631
|
executablePath,
|
|
450
632
|
env: launchEnv,
|
|
@@ -453,98 +635,137 @@ class BrowserController {
|
|
|
453
635
|
userAgent: this._userAgent,
|
|
454
636
|
locale: 'en-US',
|
|
455
637
|
ignoreHTTPSErrors: false,
|
|
638
|
+
serviceWorkers: 'block',
|
|
456
639
|
timeout: 120000,
|
|
457
640
|
});
|
|
458
|
-
|
|
641
|
+
let context;
|
|
642
|
+
try {
|
|
643
|
+
context = await raceWithSignal(launchPromise, signal);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
launchPromise.then((lateContext) => lateContext.close().catch(() => {})).catch(() => {});
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
const browser = typeof context.browser === 'function' ? context.browser() : null;
|
|
649
|
+
this.context = context;
|
|
650
|
+
this.browser = browser;
|
|
651
|
+
this._bindContextLifecycle(context, browser);
|
|
459
652
|
|
|
460
653
|
// Cloud security: deny access to local devices on every page in this context.
|
|
461
|
-
await
|
|
654
|
+
await context.addInitScript(DEVICE_DENY_SCRIPT);
|
|
655
|
+
await this._installNetworkGuard(context);
|
|
462
656
|
|
|
463
|
-
this.page =
|
|
657
|
+
this.page = context.pages()[0] || await context.newPage();
|
|
658
|
+
this._bindPage(this.page);
|
|
464
659
|
await this._applyStealthToPage(this.page);
|
|
465
660
|
})();
|
|
466
661
|
|
|
467
662
|
try {
|
|
468
|
-
await this.launchPromise;
|
|
663
|
+
await raceWithSignal(this.launchPromise, signal);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
const failedContext = this.context;
|
|
666
|
+
const failedBrowser = this.browser;
|
|
667
|
+
this._clearBrowserReferences(failedContext, failedBrowser);
|
|
668
|
+
if (failedContext) await failedContext.close().catch(() => {});
|
|
669
|
+
else if (failedBrowser) await failedBrowser.close().catch(() => {});
|
|
670
|
+
await this._stopVirtualDisplay();
|
|
671
|
+
throw error;
|
|
469
672
|
} finally {
|
|
673
|
+
externalSignal?.removeEventListener('abort', forwardAbort);
|
|
674
|
+
if (this._launchAbortController === launchAbortController) {
|
|
675
|
+
this._launchAbortController = null;
|
|
676
|
+
}
|
|
470
677
|
this.launchPromise = null;
|
|
471
678
|
this.launching = false;
|
|
472
679
|
}
|
|
473
680
|
}
|
|
474
681
|
|
|
475
|
-
async ensurePage() {
|
|
476
|
-
|
|
682
|
+
async ensurePage(options = {}) {
|
|
683
|
+
const signal = options.signal;
|
|
684
|
+
await this.ensureBrowser(options);
|
|
685
|
+
throwIfAborted(signal);
|
|
477
686
|
if (!this.page || this.page.isClosed()) {
|
|
687
|
+
let pagePromise;
|
|
478
688
|
if (this.context && typeof this.context.newPage === 'function') {
|
|
479
|
-
|
|
689
|
+
pagePromise = this.context.newPage();
|
|
480
690
|
} else {
|
|
481
|
-
|
|
691
|
+
pagePromise = this.browser.newPage();
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
this.page = await raceWithSignal(pagePromise, signal);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
pagePromise.then((latePage) => latePage.close().catch(() => {})).catch(() => {});
|
|
697
|
+
throw error;
|
|
482
698
|
}
|
|
699
|
+
this._bindPage(this.page);
|
|
483
700
|
await this._applyStealthToPage(this.page);
|
|
484
701
|
}
|
|
485
702
|
return this.page;
|
|
486
703
|
}
|
|
487
704
|
|
|
488
705
|
async takeScreenshot(options = {}) {
|
|
489
|
-
const page = await this.ensurePage();
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
706
|
+
const page = await this.ensurePage(options);
|
|
707
|
+
return this._withPageCancellation(page, options.signal, async () => {
|
|
708
|
+
let artifactRecord = null;
|
|
709
|
+
let filename = `screenshot_${Date.now()}.png`;
|
|
710
|
+
let filepath = path.join(SCREENSHOTS_DIR, filename);
|
|
711
|
+
if (this.artifactStore && this.userId != null) {
|
|
712
|
+
artifactRecord = this.artifactStore.allocateFile(this.userId, {
|
|
713
|
+
kind: 'browser-screenshot',
|
|
714
|
+
backend: this.runtimeBackend,
|
|
715
|
+
extension: 'png',
|
|
716
|
+
contentType: 'image/png',
|
|
717
|
+
filenameBase: 'browser-screenshot',
|
|
718
|
+
metadata: {
|
|
719
|
+
selector: options.selector || null,
|
|
720
|
+
fullPage: options.fullPage === true,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
filepath = artifactRecord.storagePath;
|
|
724
|
+
filename = path.basename(filepath);
|
|
725
|
+
}
|
|
508
726
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
727
|
+
const screenshotOptions = { path: filepath, type: 'png' };
|
|
728
|
+
if (options.fullPage) screenshotOptions.fullPage = true;
|
|
729
|
+
if (options.selector) {
|
|
730
|
+
const element = await page.$(options.selector);
|
|
731
|
+
if (element) {
|
|
732
|
+
await element.screenshot(screenshotOptions);
|
|
733
|
+
} else {
|
|
734
|
+
await page.screenshot(screenshotOptions);
|
|
735
|
+
}
|
|
515
736
|
} else {
|
|
516
737
|
await page.screenshot(screenshotOptions);
|
|
517
738
|
}
|
|
518
|
-
} else {
|
|
519
|
-
await page.screenshot(screenshotOptions);
|
|
520
|
-
}
|
|
521
739
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
740
|
+
if (artifactRecord) {
|
|
741
|
+
this.artifactStore.finalizeFile(artifactRecord.artifactId, filepath);
|
|
742
|
+
}
|
|
525
743
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
744
|
+
return {
|
|
745
|
+
screenshotPath: artifactRecord ? artifactRecord.url : `/screenshots/${filename}`,
|
|
746
|
+
artifactId: artifactRecord?.artifactId || null,
|
|
747
|
+
filename,
|
|
748
|
+
fullPath: filepath,
|
|
749
|
+
};
|
|
750
|
+
});
|
|
532
751
|
}
|
|
533
752
|
|
|
534
753
|
async screenshotJpeg(quality = 80, options = {}) {
|
|
535
|
-
const page = await this.ensurePage();
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
754
|
+
const page = await this.ensurePage(options);
|
|
755
|
+
return this._withPageCancellation(page, options.signal, async () => {
|
|
756
|
+
const screenshotOptions = {
|
|
757
|
+
type: 'jpeg',
|
|
758
|
+
quality: Math.min(95, Math.max(30, Math.floor(Number(quality) || 80))),
|
|
759
|
+
fullPage: options.fullPage === true,
|
|
760
|
+
};
|
|
761
|
+
if (options.selector) {
|
|
762
|
+
const element = await page.$(options.selector);
|
|
763
|
+
if (element) {
|
|
764
|
+
return element.screenshot(screenshotOptions);
|
|
765
|
+
}
|
|
545
766
|
}
|
|
546
|
-
|
|
547
|
-
|
|
767
|
+
return page.screenshot(screenshotOptions);
|
|
768
|
+
});
|
|
548
769
|
}
|
|
549
770
|
|
|
550
771
|
async _navigatePage(page, url, options = {}) {
|
|
@@ -552,9 +773,21 @@ class BrowserController {
|
|
|
552
773
|
const waitUntil = normalizeWaitUntil(options.waitUntil);
|
|
553
774
|
if (referrerMode === 'current' && page.url() && page.url() !== 'about:blank') {
|
|
554
775
|
const previousUrl = page.url();
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
776
|
+
const urlChanged = typeof page.waitForURL === 'function'
|
|
777
|
+
? page.waitForURL(
|
|
778
|
+
(nextUrl) => String(nextUrl) !== previousUrl,
|
|
779
|
+
{ waitUntil, timeout: 30000 },
|
|
780
|
+
)
|
|
781
|
+
: page.waitForFunction(
|
|
782
|
+
(oldUrl) => window.location.href !== oldUrl,
|
|
783
|
+
previousUrl,
|
|
784
|
+
{ timeout: 30000 },
|
|
785
|
+
);
|
|
786
|
+
await Promise.all([
|
|
787
|
+
urlChanged,
|
|
788
|
+
page.evaluate((nextUrl) => { window.location.href = nextUrl; }, url),
|
|
789
|
+
]);
|
|
790
|
+
await page.waitForLoadState(waitUntil, { timeout: 30000 });
|
|
558
791
|
return null;
|
|
559
792
|
}
|
|
560
793
|
|
|
@@ -581,80 +814,89 @@ class BrowserController {
|
|
|
581
814
|
}
|
|
582
815
|
|
|
583
816
|
async navigate(url, options = {}) {
|
|
584
|
-
|
|
585
|
-
|
|
817
|
+
let page = null;
|
|
586
818
|
try {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (options.waitFor) {
|
|
595
|
-
await page.waitForSelector(options.waitFor, { timeout: 10000 }).catch(() => { });
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Simulate human reading delay
|
|
599
|
-
await sleep(rand(700, 1800));
|
|
600
|
-
|
|
601
|
-
let rawHtml = await page.content();
|
|
602
|
-
const { extractForLLM } = require('./contentExtractor');
|
|
603
|
-
let extraction = extractForLLM(rawHtml, { url: page.url() });
|
|
604
|
-
let botDetection = await this._getBotDetection(page, rawHtml, extraction.markdown);
|
|
605
|
-
let challengeRetried = false;
|
|
606
|
-
|
|
607
|
-
if (
|
|
608
|
-
botDetection.detected
|
|
609
|
-
&& normalizeChallengeRetry(options.challengeRetry)
|
|
610
|
-
&& requestedReferrerMode === 'direct'
|
|
611
|
-
) {
|
|
612
|
-
challengeRetried = true;
|
|
613
|
-
activeReferrerMode = 'google';
|
|
614
|
-
await sleep(rand(1200, 2600));
|
|
615
|
-
response = await this._navigatePage(page, url, {
|
|
819
|
+
await this._assertNavigationAllowed(url, options);
|
|
820
|
+
page = await this.ensurePage(options);
|
|
821
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
822
|
+
const requestedReferrerMode = normalizeReferrerMode(options.referrerMode);
|
|
823
|
+
let activeReferrerMode = requestedReferrerMode;
|
|
824
|
+
let response = await this._navigatePage(page, url, {
|
|
616
825
|
...options,
|
|
617
826
|
referrerMode: activeReferrerMode,
|
|
618
827
|
});
|
|
828
|
+
|
|
619
829
|
if (options.waitFor) {
|
|
620
|
-
await page.waitForSelector(options.waitFor, { timeout: 10000 })
|
|
830
|
+
await page.waitForSelector(options.waitFor, { timeout: 10000 });
|
|
621
831
|
}
|
|
622
|
-
await sleep(rand(900, 2200));
|
|
623
|
-
rawHtml = await page.content();
|
|
624
|
-
extraction = extractForLLM(rawHtml, { url: page.url() });
|
|
625
|
-
botDetection = await this._getBotDetection(page, rawHtml, extraction.markdown);
|
|
626
|
-
}
|
|
627
832
|
|
|
628
|
-
|
|
629
|
-
|
|
833
|
+
// Simulate human reading delay.
|
|
834
|
+
await sleep(rand(700, 1800), options.signal);
|
|
835
|
+
|
|
836
|
+
let rawHtml = await page.content();
|
|
837
|
+
const { extractForLLM } = require('./contentExtractor');
|
|
838
|
+
let extraction = extractForLLM(rawHtml, { url: page.url() });
|
|
839
|
+
let botDetection = await this._getBotDetection(page, rawHtml, extraction.markdown);
|
|
840
|
+
let challengeRetried = false;
|
|
841
|
+
|
|
842
|
+
if (
|
|
843
|
+
botDetection.detected
|
|
844
|
+
&& normalizeChallengeRetry(options.challengeRetry)
|
|
845
|
+
&& requestedReferrerMode === 'direct'
|
|
846
|
+
) {
|
|
847
|
+
challengeRetried = true;
|
|
848
|
+
activeReferrerMode = 'google';
|
|
849
|
+
await sleep(rand(1200, 2600), options.signal);
|
|
850
|
+
response = await this._navigatePage(page, url, {
|
|
851
|
+
...options,
|
|
852
|
+
referrerMode: activeReferrerMode,
|
|
853
|
+
});
|
|
854
|
+
if (options.waitFor) {
|
|
855
|
+
await page.waitForSelector(options.waitFor, { timeout: 10000 });
|
|
856
|
+
}
|
|
857
|
+
await sleep(rand(900, 2200), options.signal);
|
|
858
|
+
rawHtml = await page.content();
|
|
859
|
+
extraction = extractForLLM(rawHtml, { url: page.url() });
|
|
860
|
+
botDetection = await this._getBotDetection(page, rawHtml, extraction.markdown);
|
|
861
|
+
}
|
|
630
862
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
screenshot = await this.takeScreenshot({ fullPage: options.fullPage });
|
|
634
|
-
}
|
|
863
|
+
const title = await page.title();
|
|
864
|
+
const currentUrl = page.url();
|
|
635
865
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
866
|
+
let screenshot = null;
|
|
867
|
+
if (options.screenshot !== false) {
|
|
868
|
+
screenshot = await this.takeScreenshot({
|
|
869
|
+
fullPage: options.fullPage,
|
|
870
|
+
signal: options.signal,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
title,
|
|
876
|
+
url: currentUrl,
|
|
877
|
+
status: response?.status() || 0,
|
|
878
|
+
pageContent: extraction.markdown,
|
|
879
|
+
botDetection,
|
|
880
|
+
referrerMode: activeReferrerMode,
|
|
881
|
+
challengeRetried,
|
|
882
|
+
screenshotPath: screenshot?.screenshotPath || null,
|
|
883
|
+
artifactId: screenshot?.artifactId || null,
|
|
884
|
+
fullPath: screenshot?.fullPath || null,
|
|
885
|
+
};
|
|
886
|
+
});
|
|
648
887
|
} catch (err) {
|
|
888
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
649
889
|
let screenshot = null;
|
|
650
|
-
|
|
890
|
+
if (page && !page.isClosed()) {
|
|
891
|
+
try { screenshot = await this.takeScreenshot(); } catch {}
|
|
892
|
+
}
|
|
651
893
|
return {
|
|
652
894
|
error: err.message,
|
|
653
895
|
url,
|
|
654
896
|
botDetection: { detected: false, provider: null },
|
|
655
897
|
screenshotPath: screenshot?.screenshotPath || null,
|
|
656
898
|
artifactId: screenshot?.artifactId || null,
|
|
657
|
-
fullPath: screenshot?.fullPath || null
|
|
899
|
+
fullPath: screenshot?.fullPath || null,
|
|
658
900
|
};
|
|
659
901
|
}
|
|
660
902
|
}
|
|
@@ -668,7 +910,7 @@ class BrowserController {
|
|
|
668
910
|
for (const point of path) {
|
|
669
911
|
await page.mouse.move(point.x, point.y);
|
|
670
912
|
if (!options.fast) {
|
|
671
|
-
await sleep(rand(2, 10));
|
|
913
|
+
await sleep(rand(2, 10), options.signal);
|
|
672
914
|
}
|
|
673
915
|
}
|
|
674
916
|
this._mousePosition = target;
|
|
@@ -689,271 +931,327 @@ class BrowserController {
|
|
|
689
931
|
};
|
|
690
932
|
}
|
|
691
933
|
|
|
692
|
-
async click(selector, text, screenshot = true) {
|
|
693
|
-
const page = await this.ensurePage();
|
|
934
|
+
async click(selector, text, screenshot = true, options = {}) {
|
|
935
|
+
const page = await this.ensurePage(options);
|
|
694
936
|
|
|
695
937
|
try {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
938
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
939
|
+
let target = null;
|
|
940
|
+
|
|
941
|
+
if (text && !selector) {
|
|
942
|
+
const elements = await page.$$('a, button, [role="button"], input[type="submit"], [onclick]');
|
|
943
|
+
for (const el of elements) {
|
|
944
|
+
const elText = await page.evaluate(e => e.innerText || e.value || e.getAttribute('aria-label') || '', el);
|
|
945
|
+
if (elText.toLowerCase().includes(String(text).toLowerCase())) {
|
|
946
|
+
target = el;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
705
949
|
}
|
|
950
|
+
if (!target) return { error: `No clickable element found with text: ${text}` };
|
|
951
|
+
} else if (selector) {
|
|
952
|
+
target = await page.$(selector);
|
|
953
|
+
if (!target) return { error: `Element not found: ${selector}` };
|
|
954
|
+
} else {
|
|
955
|
+
return { error: 'Either selector or text required' };
|
|
706
956
|
}
|
|
707
|
-
if (!target) return { error: `No clickable element found with text: ${text}` };
|
|
708
|
-
} else if (selector) {
|
|
709
|
-
target = await page.$(selector);
|
|
710
|
-
if (!target) return { error: `Element not found: ${selector}` };
|
|
711
|
-
} else {
|
|
712
|
-
return { error: 'Either selector or text required' };
|
|
713
|
-
}
|
|
714
957
|
|
|
715
|
-
|
|
716
|
-
|
|
958
|
+
const point = await this._pointForElement(target);
|
|
959
|
+
if (!point) return { error: 'Element has no visible clickable area' };
|
|
717
960
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
961
|
+
await this._moveMouseTo(page, point.x, point.y, { signal: options.signal });
|
|
962
|
+
await sleep(rand(170, 320), options.signal);
|
|
963
|
+
await page.mouse.down();
|
|
964
|
+
await sleep(rand(80, 260), options.signal);
|
|
965
|
+
await page.mouse.up();
|
|
723
966
|
|
|
724
|
-
|
|
967
|
+
await sleep(rand(800, 1800), options.signal);
|
|
725
968
|
|
|
726
|
-
|
|
727
|
-
|
|
969
|
+
let screenshotResult = null;
|
|
970
|
+
if (screenshot) screenshotResult = await this.takeScreenshot({ signal: options.signal });
|
|
728
971
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
972
|
+
return {
|
|
973
|
+
success: true,
|
|
974
|
+
url: page.url(),
|
|
975
|
+
title: await page.title(),
|
|
976
|
+
screenshotPath: screenshotResult?.screenshotPath || null,
|
|
977
|
+
artifactId: screenshotResult?.artifactId || null,
|
|
978
|
+
fullPath: screenshotResult?.fullPath || null,
|
|
979
|
+
};
|
|
980
|
+
});
|
|
737
981
|
} catch (err) {
|
|
982
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
738
983
|
return { error: err.message };
|
|
739
984
|
}
|
|
740
985
|
}
|
|
741
986
|
|
|
742
|
-
async clickPoint(x, y, screenshot = true) {
|
|
743
|
-
const page = await this.ensurePage();
|
|
987
|
+
async clickPoint(x, y, screenshot = true, options = {}) {
|
|
988
|
+
const page = await this.ensurePage(options);
|
|
744
989
|
|
|
745
990
|
try {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
991
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
992
|
+
const px = Math.max(0, normalizePointCoordinate(x, 'x'));
|
|
993
|
+
const py = Math.max(0, normalizePointCoordinate(y, 'y'));
|
|
994
|
+
await this._moveMouseTo(page, px, py, { signal: options.signal });
|
|
995
|
+
await sleep(rand(90, 220), options.signal);
|
|
996
|
+
await page.mouse.down();
|
|
997
|
+
await sleep(rand(70, 240), options.signal);
|
|
998
|
+
await page.mouse.up();
|
|
999
|
+
await sleep(rand(500, 1200), options.signal);
|
|
1000
|
+
|
|
1001
|
+
let screenshotResult = null;
|
|
1002
|
+
if (screenshot) screenshotResult = await this.takeScreenshot({ signal: options.signal });
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
success: true,
|
|
1006
|
+
x: px,
|
|
1007
|
+
y: py,
|
|
1008
|
+
url: page.url(),
|
|
1009
|
+
title: await page.title(),
|
|
1010
|
+
screenshotPath: screenshotResult?.screenshotPath || null,
|
|
1011
|
+
artifactId: screenshotResult?.artifactId || null,
|
|
1012
|
+
fullPath: screenshotResult?.fullPath || null,
|
|
1013
|
+
};
|
|
1014
|
+
});
|
|
768
1015
|
} catch (err) {
|
|
1016
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
769
1017
|
return { error: err.message };
|
|
770
1018
|
}
|
|
771
1019
|
}
|
|
772
1020
|
|
|
773
1021
|
async hoverPoint(x, y, options = {}) {
|
|
774
|
-
const page = await this.ensurePage();
|
|
1022
|
+
const page = await this.ensurePage(options);
|
|
775
1023
|
try {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1024
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
1025
|
+
const px = Math.max(0, normalizePointCoordinate(x, 'x'));
|
|
1026
|
+
const py = Math.max(0, normalizePointCoordinate(y, 'y'));
|
|
1027
|
+
await this._moveMouseTo(page, px, py, {
|
|
1028
|
+
fast: Number(options.steps) <= 1,
|
|
1029
|
+
signal: options.signal,
|
|
1030
|
+
});
|
|
1031
|
+
return {
|
|
1032
|
+
success: true,
|
|
1033
|
+
x: px,
|
|
1034
|
+
y: py,
|
|
1035
|
+
url: page.url(),
|
|
1036
|
+
title: await page.title(),
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
786
1039
|
} catch (err) {
|
|
1040
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
787
1041
|
return { error: err.message };
|
|
788
1042
|
}
|
|
789
1043
|
}
|
|
790
1044
|
|
|
791
|
-
async scroll(deltaX = 0, deltaY = 0, screenshot = true) {
|
|
792
|
-
const page = await this.ensurePage();
|
|
1045
|
+
async scroll(deltaX = 0, deltaY = 0, screenshot = true, options = {}) {
|
|
1046
|
+
const page = await this.ensurePage(options);
|
|
793
1047
|
|
|
794
1048
|
try {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1049
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
1050
|
+
const x = Math.max(10, Math.min(this._viewport.width - 10, this._mousePosition.x + rand(-80, 80)));
|
|
1051
|
+
const y = Math.max(10, Math.min(this._viewport.height - 10, this._mousePosition.y + rand(-80, 80)));
|
|
1052
|
+
await this._moveMouseTo(page, x, y, { signal: options.signal });
|
|
1053
|
+
const totalX = Math.round(Number(deltaX) || 0);
|
|
1054
|
+
const totalY = Math.round(Number(deltaY) || 0);
|
|
1055
|
+
const chunks = Math.max(1, Math.min(6, Math.ceil(Math.max(Math.abs(totalX), Math.abs(totalY)) / 450)));
|
|
1056
|
+
for (let i = 0; i < chunks; i += 1) {
|
|
1057
|
+
await page.mouse.wheel({
|
|
1058
|
+
deltaX: Math.round(totalX / chunks),
|
|
1059
|
+
deltaY: Math.round(totalY / chunks),
|
|
1060
|
+
});
|
|
1061
|
+
await sleep(rand(120, 360), options.signal);
|
|
1062
|
+
}
|
|
808
1063
|
|
|
809
|
-
|
|
810
|
-
|
|
1064
|
+
let screenshotResult = null;
|
|
1065
|
+
if (screenshot) screenshotResult = await this.takeScreenshot({ signal: options.signal });
|
|
811
1066
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1067
|
+
return {
|
|
1068
|
+
success: true,
|
|
1069
|
+
url: page.url(),
|
|
1070
|
+
title: await page.title(),
|
|
1071
|
+
screenshotPath: screenshotResult?.screenshotPath || null,
|
|
1072
|
+
artifactId: screenshotResult?.artifactId || null,
|
|
1073
|
+
fullPath: screenshotResult?.fullPath || null,
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
820
1076
|
} catch (err) {
|
|
1077
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
821
1078
|
return { error: err.message };
|
|
822
1079
|
}
|
|
823
1080
|
}
|
|
824
1081
|
|
|
825
1082
|
async type(selector, text, options = {}) {
|
|
826
|
-
const page = await this.ensurePage();
|
|
1083
|
+
const page = await this.ensurePage(options);
|
|
827
1084
|
|
|
828
1085
|
try {
|
|
829
|
-
|
|
1086
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
1087
|
+
const value = String(text ?? '');
|
|
830
1088
|
const element = await page.$(selector);
|
|
831
|
-
if (element) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1089
|
+
if (!element) return { error: `Element not found: ${selector}` };
|
|
1090
|
+
const point = await this._pointForElement(element);
|
|
1091
|
+
if (point) {
|
|
1092
|
+
await this._moveMouseTo(page, point.x, point.y, { signal: options.signal });
|
|
1093
|
+
await sleep(rand(120, 260), options.signal);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const locator = typeof page.locator === 'function' ? page.locator(selector) : null;
|
|
1097
|
+
if (options.clear !== false) {
|
|
1098
|
+
if (locator && typeof locator.fill === 'function') {
|
|
1099
|
+
await locator.fill('');
|
|
1100
|
+
} else {
|
|
1101
|
+
await page.click(selector, { clickCount: 3, delay: rand(40, 120) });
|
|
1102
|
+
await page.keyboard.press('Backspace');
|
|
836
1103
|
}
|
|
837
1104
|
}
|
|
838
|
-
await page.click(selector, { clickCount: 3, delay: rand(40, 120) });
|
|
839
|
-
await page.keyboard.press('Backspace');
|
|
840
|
-
}
|
|
841
1105
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1106
|
+
if (locator && typeof locator.pressSequentially === 'function') {
|
|
1107
|
+
await locator.pressSequentially(value, { delay: rand(45, 140) });
|
|
1108
|
+
} else {
|
|
1109
|
+
await page.type(selector, value, { delay: rand(45, 140) });
|
|
1110
|
+
}
|
|
845
1111
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1112
|
+
if (options.pressEnter) {
|
|
1113
|
+
if (locator && typeof locator.press === 'function') await locator.press('Enter');
|
|
1114
|
+
else await page.keyboard.press('Enter');
|
|
1115
|
+
await sleep(1000, options.signal);
|
|
1116
|
+
}
|
|
850
1117
|
|
|
851
|
-
|
|
852
|
-
|
|
1118
|
+
let screenshotResult = null;
|
|
1119
|
+
if (options.screenshot !== false) {
|
|
1120
|
+
screenshotResult = await this.takeScreenshot({ signal: options.signal });
|
|
1121
|
+
}
|
|
853
1122
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1123
|
+
return {
|
|
1124
|
+
success: true,
|
|
1125
|
+
typed: value,
|
|
1126
|
+
screenshotPath: screenshotResult?.screenshotPath || null,
|
|
1127
|
+
artifactId: screenshotResult?.artifactId || null,
|
|
1128
|
+
fullPath: screenshotResult?.fullPath || null,
|
|
1129
|
+
};
|
|
1130
|
+
});
|
|
861
1131
|
} catch (err) {
|
|
1132
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
862
1133
|
return { error: err.message };
|
|
863
1134
|
}
|
|
864
1135
|
}
|
|
865
1136
|
|
|
866
1137
|
async typeText(text, options = {}) {
|
|
867
|
-
const page = await this.ensurePage();
|
|
1138
|
+
const page = await this.ensurePage(options);
|
|
868
1139
|
|
|
869
1140
|
try {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1141
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
1142
|
+
const value = String(text ?? '');
|
|
1143
|
+
await page.keyboard.type(value, { delay: rand(45, 140) });
|
|
873
1144
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1145
|
+
if (options.pressEnter) {
|
|
1146
|
+
await page.keyboard.press('Enter');
|
|
1147
|
+
await sleep(800, options.signal);
|
|
1148
|
+
}
|
|
878
1149
|
|
|
879
|
-
|
|
880
|
-
|
|
1150
|
+
let screenshotResult = null;
|
|
1151
|
+
if (options.screenshot !== false) {
|
|
1152
|
+
screenshotResult = await this.takeScreenshot({ signal: options.signal });
|
|
1153
|
+
}
|
|
881
1154
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1155
|
+
return {
|
|
1156
|
+
success: true,
|
|
1157
|
+
typed: value,
|
|
1158
|
+
screenshotPath: screenshotResult?.screenshotPath || null,
|
|
1159
|
+
artifactId: screenshotResult?.artifactId || null,
|
|
1160
|
+
fullPath: screenshotResult?.fullPath || null,
|
|
1161
|
+
};
|
|
1162
|
+
});
|
|
889
1163
|
} catch (err) {
|
|
1164
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
890
1165
|
return { error: err.message };
|
|
891
1166
|
}
|
|
892
1167
|
}
|
|
893
1168
|
|
|
894
|
-
async pressKey(key, screenshot = true) {
|
|
895
|
-
const page = await this.ensurePage();
|
|
1169
|
+
async pressKey(key, screenshot = true, options = {}) {
|
|
1170
|
+
const page = await this.ensurePage(options);
|
|
896
1171
|
|
|
897
1172
|
try {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1173
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
1174
|
+
const normalized = String(key || '').trim();
|
|
1175
|
+
if (!normalized) {
|
|
1176
|
+
return { error: 'key required' };
|
|
1177
|
+
}
|
|
1178
|
+
await page.keyboard.press(normalized);
|
|
1179
|
+
await sleep(rand(250, 700), options.signal);
|
|
1180
|
+
|
|
1181
|
+
let screenshotResult = null;
|
|
1182
|
+
if (screenshot) screenshotResult = await this.takeScreenshot({ signal: options.signal });
|
|
1183
|
+
|
|
1184
|
+
return {
|
|
1185
|
+
success: true,
|
|
1186
|
+
key: normalized,
|
|
1187
|
+
screenshotPath: screenshotResult?.screenshotPath || null,
|
|
1188
|
+
artifactId: screenshotResult?.artifactId || null,
|
|
1189
|
+
fullPath: screenshotResult?.fullPath || null,
|
|
1190
|
+
};
|
|
1191
|
+
});
|
|
915
1192
|
} catch (err) {
|
|
1193
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
916
1194
|
return { error: err.message };
|
|
917
1195
|
}
|
|
918
1196
|
}
|
|
919
1197
|
|
|
920
|
-
async extract(selector, attribute, all = false) {
|
|
921
|
-
const page = await this.ensurePage();
|
|
1198
|
+
async extract(selector, attribute, all = false, options = {}) {
|
|
1199
|
+
const page = await this.ensurePage(options);
|
|
922
1200
|
|
|
923
1201
|
try {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1202
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
1203
|
+
const rawHtml = await page.content().catch(() => '');
|
|
1204
|
+
const botDetection = await this._getBotDetection(page, rawHtml, '')
|
|
1205
|
+
.catch(() => ({ detected: false, provider: null }));
|
|
1206
|
+
if (all) {
|
|
1207
|
+
const results = await page.$$eval(selector || 'body', (elements, attr) => {
|
|
1208
|
+
return elements.slice(0, 100).map(el => {
|
|
1209
|
+
let value = '';
|
|
1210
|
+
if (attr === 'innerHTML') value = el.innerHTML;
|
|
1211
|
+
else if (attr === 'outerHTML') value = el.outerHTML;
|
|
1212
|
+
else if (attr) value = el.getAttribute(attr) || '';
|
|
1213
|
+
else value = el.innerText || '';
|
|
1214
|
+
return String(value).slice(0, 50000);
|
|
1215
|
+
});
|
|
1216
|
+
}, attribute);
|
|
1217
|
+
return { results, botDetection };
|
|
1218
|
+
}
|
|
937
1219
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1220
|
+
const result = await page.$eval(selector || 'body', (el, attr) => {
|
|
1221
|
+
if (attr === 'innerHTML') return el.innerHTML;
|
|
1222
|
+
if (attr === 'outerHTML') return el.outerHTML;
|
|
1223
|
+
if (attr) return el.getAttribute(attr) || '';
|
|
1224
|
+
return el.innerText || '';
|
|
1225
|
+
}, attribute);
|
|
944
1226
|
|
|
945
|
-
|
|
1227
|
+
return { result: typeof result === 'string' ? result.slice(0, 50000) : result, botDetection };
|
|
1228
|
+
});
|
|
946
1229
|
} catch (err) {
|
|
1230
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
947
1231
|
return { error: err.message };
|
|
948
1232
|
}
|
|
949
1233
|
}
|
|
950
1234
|
|
|
951
|
-
async evaluate(script) {
|
|
952
|
-
const
|
|
1235
|
+
async evaluate(script, options = {}) {
|
|
1236
|
+
const source = String(script || '');
|
|
1237
|
+
if (!source) return { error: 'script required' };
|
|
1238
|
+
if (source.length > 10000) return { error: 'script exceeds maximum length (10000)' };
|
|
1239
|
+
const page = await this.ensurePage(options);
|
|
953
1240
|
try {
|
|
954
|
-
|
|
955
|
-
|
|
1241
|
+
return await this._withPageCancellation(page, options.signal, async () => {
|
|
1242
|
+
const result = await page.evaluate(buildIsolatedEvaluationExpression(source));
|
|
1243
|
+
const serialized = typeof result === 'object' && result !== null
|
|
1244
|
+
? JSON.stringify(result)
|
|
1245
|
+
: String(result);
|
|
1246
|
+
const output = serialized ?? 'undefined';
|
|
1247
|
+
const maxChars = 1024 * 1024;
|
|
1248
|
+
return {
|
|
1249
|
+
result: output.slice(0, maxChars),
|
|
1250
|
+
truncated: output.length > maxChars,
|
|
1251
|
+
};
|
|
1252
|
+
});
|
|
956
1253
|
} catch (err) {
|
|
1254
|
+
if (isAbortError(err, options.signal)) throw err;
|
|
957
1255
|
return { error: err.message };
|
|
958
1256
|
}
|
|
959
1257
|
}
|
|
@@ -963,14 +1261,12 @@ class BrowserController {
|
|
|
963
1261
|
}
|
|
964
1262
|
|
|
965
1263
|
async launch(options = {}) {
|
|
966
|
-
|
|
967
|
-
await this.ensureBrowser();
|
|
1264
|
+
await this.ensureBrowser(options);
|
|
968
1265
|
return { success: true };
|
|
969
1266
|
}
|
|
970
1267
|
|
|
971
1268
|
isLaunched() {
|
|
972
|
-
|
|
973
|
-
return !!(this.browser && typeof this.browser.isConnected === 'function' && this.browser.isConnected());
|
|
1269
|
+
return this._contextIsOpen();
|
|
974
1270
|
}
|
|
975
1271
|
|
|
976
1272
|
getPageCount() {
|
|
@@ -981,84 +1277,139 @@ class BrowserController {
|
|
|
981
1277
|
try { return this.browser.pages ? 1 : 0; } catch { return 0; }
|
|
982
1278
|
}
|
|
983
1279
|
|
|
984
|
-
async fill(selector, value) {
|
|
985
|
-
return this.type(selector, String(value));
|
|
1280
|
+
async fill(selector, value, options = {}) {
|
|
1281
|
+
return this.type(selector, String(value), options);
|
|
986
1282
|
}
|
|
987
1283
|
|
|
988
1284
|
async extractContent(options = {}) {
|
|
989
|
-
return this.extract(options.selector, options.attribute, options.all);
|
|
1285
|
+
return this.extract(options.selector, options.attribute, options.all, options);
|
|
990
1286
|
}
|
|
991
1287
|
|
|
992
|
-
async executeJS(code) {
|
|
993
|
-
return this.evaluate(code);
|
|
1288
|
+
async executeJS(code, options = {}) {
|
|
1289
|
+
return this.evaluate(code, options);
|
|
994
1290
|
}
|
|
995
1291
|
|
|
996
|
-
async getPageInfo() {
|
|
1292
|
+
async getPageInfo(options = {}) {
|
|
1293
|
+
throwIfAborted(options.signal);
|
|
997
1294
|
if (!this.page || this.page.isClosed()) return { url: null, title: null };
|
|
998
1295
|
return {
|
|
999
1296
|
url: this.page.url(),
|
|
1000
|
-
title: await this.page.title()
|
|
1297
|
+
title: await raceWithSignal(this.page.title(), options.signal),
|
|
1001
1298
|
};
|
|
1002
1299
|
}
|
|
1003
1300
|
|
|
1004
|
-
async getCookies() {
|
|
1005
|
-
await this.ensureBrowser();
|
|
1301
|
+
async getCookies(options = {}) {
|
|
1302
|
+
await this.ensureBrowser(options);
|
|
1006
1303
|
if (!this.context || typeof this.context.cookies !== 'function') {
|
|
1007
1304
|
return { cookies: [] };
|
|
1008
1305
|
}
|
|
1009
|
-
const cookies = await this.context.cookies().
|
|
1306
|
+
const cookies = await raceWithSignal(this.context.cookies(), options.signal);
|
|
1010
1307
|
return {
|
|
1011
1308
|
cookies: Array.isArray(cookies) ? cookies : [],
|
|
1012
1309
|
};
|
|
1013
1310
|
}
|
|
1014
1311
|
|
|
1015
1312
|
async close() {
|
|
1016
|
-
if (this.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1313
|
+
if (this._closePromise) return this._closePromise;
|
|
1314
|
+
this._closing = true;
|
|
1315
|
+
this._launchAbortController?.abort(new Error('Browser controller is closing.'));
|
|
1316
|
+
this._closePromise = (async () => {
|
|
1317
|
+
const launchPromise = this.launchPromise;
|
|
1318
|
+
if (launchPromise) await launchPromise.catch(() => {});
|
|
1319
|
+
|
|
1320
|
+
const page = this.page;
|
|
1321
|
+
const context = this.context;
|
|
1322
|
+
const browser = this.browser;
|
|
1023
1323
|
this.page = null;
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
if (this.browser) {
|
|
1027
|
-
await this.browser.close().catch(() => { });
|
|
1324
|
+
this.context = null;
|
|
1028
1325
|
this.browser = null;
|
|
1029
|
-
|
|
1326
|
+
|
|
1327
|
+
if (context) {
|
|
1328
|
+
await context.close({ reason: 'Browser controller closed' }).catch(() => {});
|
|
1329
|
+
} else if (browser) {
|
|
1330
|
+
await browser.close().catch(() => {});
|
|
1331
|
+
} else if (page && !page.isClosed()) {
|
|
1332
|
+
await page.close({ runBeforeUnload: false, reason: 'Browser controller closed' }).catch(() => {});
|
|
1333
|
+
}
|
|
1334
|
+
await this._stopVirtualDisplay();
|
|
1335
|
+
})();
|
|
1336
|
+
try {
|
|
1337
|
+
await this._closePromise;
|
|
1338
|
+
} finally {
|
|
1339
|
+
this._closePromise = null;
|
|
1340
|
+
this._closing = false;
|
|
1030
1341
|
}
|
|
1031
1342
|
}
|
|
1032
1343
|
|
|
1033
|
-
async ensureVirtualDisplay() {
|
|
1344
|
+
async ensureVirtualDisplay(options = {}) {
|
|
1034
1345
|
if (process.platform !== 'linux') {
|
|
1035
1346
|
return;
|
|
1036
1347
|
}
|
|
1037
|
-
if (this.displayProcess &&
|
|
1348
|
+
if (this.displayProcess && this.displayProcess.exitCode == null) {
|
|
1038
1349
|
return;
|
|
1039
1350
|
}
|
|
1040
1351
|
if (this.displayValue && String(this.displayValue).trim()) {
|
|
1041
1352
|
return;
|
|
1042
1353
|
}
|
|
1043
1354
|
|
|
1044
|
-
const display =
|
|
1355
|
+
const display = chooseVirtualDisplay();
|
|
1045
1356
|
const child = spawn('Xvfb', [display, '-screen', '0', '1440x900x24', '-ac', '-nolisten', 'tcp'], {
|
|
1046
1357
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
1047
1358
|
});
|
|
1359
|
+
this.displayProcess = child;
|
|
1048
1360
|
|
|
1049
1361
|
let launchError = '';
|
|
1050
1362
|
child.stderr.on('data', (chunk) => {
|
|
1051
|
-
launchError += chunk.toString();
|
|
1363
|
+
if (launchError.length < 64 * 1024) launchError += chunk.toString();
|
|
1364
|
+
});
|
|
1365
|
+
child.once('close', () => {
|
|
1366
|
+
if (this.displayProcess === child) {
|
|
1367
|
+
this.displayProcess = null;
|
|
1368
|
+
if (this.displayValue === display) this.displayValue = process.env.DISPLAY || null;
|
|
1369
|
+
if (this._managedDisplayValue === display) this._managedDisplayValue = null;
|
|
1370
|
+
}
|
|
1052
1371
|
});
|
|
1053
1372
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1373
|
+
try {
|
|
1374
|
+
await Promise.race([
|
|
1375
|
+
sleep(1000, options.signal),
|
|
1376
|
+
new Promise((_, reject) => child.once('error', reject)),
|
|
1377
|
+
]);
|
|
1378
|
+
if (child.exitCode != null) {
|
|
1379
|
+
throw new Error(`Failed to start Xvfb: ${String(launchError || `exit code ${child.exitCode}`).trim()}`);
|
|
1380
|
+
}
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
await this._stopVirtualDisplay();
|
|
1383
|
+
throw error;
|
|
1057
1384
|
}
|
|
1058
1385
|
|
|
1059
|
-
this.displayProcess = child;
|
|
1060
1386
|
this.displayValue = display;
|
|
1387
|
+
this._managedDisplayValue = display;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
async _stopVirtualDisplay() {
|
|
1391
|
+
const child = this.displayProcess;
|
|
1392
|
+
const managedDisplay = this._managedDisplayValue;
|
|
1393
|
+
this.displayProcess = null;
|
|
1394
|
+
this._managedDisplayValue = null;
|
|
1395
|
+
if (managedDisplay && this.displayValue === managedDisplay) {
|
|
1396
|
+
this.displayValue = process.env.DISPLAY || null;
|
|
1397
|
+
}
|
|
1398
|
+
if (!child || child.exitCode != null) return;
|
|
1399
|
+
|
|
1400
|
+
const exited = new Promise((resolve) => child.once('close', resolve));
|
|
1401
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
1402
|
+
await Promise.race([exited, sleep(1000)]);
|
|
1403
|
+
if (child.exitCode == null) {
|
|
1404
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
1405
|
+
await Promise.race([exited, sleep(1000)]);
|
|
1406
|
+
}
|
|
1061
1407
|
}
|
|
1062
1408
|
}
|
|
1063
1409
|
|
|
1064
|
-
module.exports = {
|
|
1410
|
+
module.exports = {
|
|
1411
|
+
BrowserController,
|
|
1412
|
+
buildIsolatedEvaluationExpression,
|
|
1413
|
+
normalizeWaitUntil,
|
|
1414
|
+
resolveBrowserExecutablePath,
|
|
1415
|
+
};
|