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