@jshookmcp/jshook 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-CqGDXmfc.mjs} +160 -54
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-BdjRfNx7.mjs} +5 -5
- package/dist/ConsoleMonitor-DykL3IAw.mjs +2269 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-ETyy0xyo.mjs} +1 -1
- package/dist/DetailedDataManager-HT49OrvF.mjs +217 -0
- package/dist/EventBus-DFKvADm3.mjs +141 -0
- package/dist/EvidenceGraphBridge-318Oi0Lf.mjs +153 -0
- package/dist/{ExtensionManager-D5-bO9D8.mjs → ExtensionManager-BDMsY2Dz.mjs} +27 -13
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-BN4UQWnX.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-Cc2AFq1Y.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-DruMgsgj.mjs} +21 -21
- package/dist/HookGeneratorBuilders.core.generators.storage-CTbB4Lcx.mjs +566 -0
- package/dist/InstrumentationSession-DLH0vd-z.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CMtviNW_.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-ITgb_NMi.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-CiL7Z3ey.mjs} +50 -21
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-D9Lkovvn.mjs} +20 -56
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-BBlAixF5.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-DMQ44gen.mjs} +16 -16
- package/dist/PageController-BPJNqqBN.mjs +431 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-K7wN8Z-w.mjs} +10 -7
- package/dist/PrerequisiteError-TuyZIs6n.mjs +20 -0
- package/dist/ProcessRegistry-zGg12QbE.mjs +74 -0
- package/dist/ResponseBuilder-CJXWmWNw.mjs +143 -0
- package/dist/ReverseEvidenceGraph-C02-gXOh.mjs +269 -0
- package/dist/ScriptManager-ZuWD-0Jg.mjs +3003 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-D-z0umeT.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-Cav5AVSL.mjs} +9 -6
- package/dist/ToolCatalog-5OJdMiF0.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-DbCFGyrg.mjs +45 -0
- package/dist/ToolRegistry-B9krbTtI.mjs +180 -0
- package/dist/ToolRouter.policy-BGDAGyeH.mjs +344 -0
- package/dist/TraceRecorder-B41Z5XBj.mjs +1286 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-C2kjj0ze.mjs} +19 -13
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-CKrGOTpo.mjs} +3 -3
- package/dist/WorkflowEngine-DJ6M4opp.mjs +569 -0
- package/dist/analysis-BHeJW2Nb.mjs +1234 -0
- package/dist/antidebug-BRKeyt27.mjs +1081 -0
- package/dist/artifactRetention-CPXkUJXp.mjs +598 -0
- package/dist/artifacts-DkfosXH3.mjs +59 -0
- package/dist/authorization-schema-DRqyJMSk.mjs +31 -0
- package/dist/betterSqlite3-DLSBZodi.mjs +74 -0
- package/dist/binary-instrument--V3MAhJ4.mjs +971 -0
- package/dist/bind-helpers-ClV34xdn.mjs +42 -0
- package/dist/boringssl-inspector-Bo_LOLaS.mjs +180 -0
- package/dist/browser-Dx3_S2cG.mjs +4369 -0
- package/dist/capabilities-CcHlvWgK.mjs +33 -0
- package/dist/concurrency-Drev_Vz9.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-CDZLOoVv.mjs} +105 -48
- package/dist/coordination-DgItD9DL.mjs +259 -0
- package/dist/debugger-RS3RSAqs.mjs +1288 -0
- package/dist/definitions-BEoYofW5.mjs +47 -0
- package/dist/definitions-BRaefg3u.mjs +365 -0
- package/dist/definitions-BbkvZkiv.mjs +96 -0
- package/dist/definitions-BtWSHJ3o.mjs +17 -0
- package/dist/definitions-C1gCHO0i.mjs +43 -0
- package/dist/definitions-CDOg_b-l.mjs +138 -0
- package/dist/definitions-CVPD9hzZ.mjs +54 -0
- package/dist/definitions-Cea8Lgl7.mjs +94 -0
- package/dist/definitions-DAgIyjxM.mjs +10 -0
- package/dist/definitions-DJA27nsL.mjs +66 -0
- package/dist/definitions-DKPFU3LW.mjs +25 -0
- package/dist/definitions-DPRpZQ96.mjs +47 -0
- package/dist/definitions-DUE5gmdn.mjs +18 -0
- package/dist/definitions-DYVjOtxa.mjs +26 -0
- package/dist/definitions-DcYLVLCo.mjs +37 -0
- package/dist/definitions-Pp5LI2H4.mjs +27 -0
- package/dist/definitions-j9KdHVNR.mjs +14 -0
- package/dist/definitions-uzkjBwa7.mjs +258 -0
- package/dist/definitions-va-AnLuQ.mjs +28 -0
- package/dist/encoding-DJeqHmpd.mjs +1079 -0
- package/dist/evidence-graph-bridge-DcYizFk2.mjs +136 -0
- package/dist/{factory-CibqTNC8.mjs → factory-C90tBff6.mjs} +41 -56
- package/dist/flat-target-session-Dgax2Cy3.mjs +29 -0
- package/dist/graphql-CoHrhweh.mjs +1197 -0
- package/dist/handlers-4jmR0nMs.mjs +898 -0
- package/dist/handlers-BAHPxcch.mjs +789 -0
- package/dist/handlers-BOs9b907.mjs +2600 -0
- package/dist/handlers-BWXEy6ef.mjs +917 -0
- package/dist/handlers-Bndn6QvE.mjs +111 -0
- package/dist/handlers-BqC4bD4s.mjs +681 -0
- package/dist/handlers-BtYq60bM2.mjs +276 -0
- package/dist/handlers-BzgcB4iv.mjs +799 -0
- package/dist/handlers-CRyRWj2b.mjs +859 -0
- package/dist/handlers-CVv2H1uq.mjs +592 -0
- package/dist/handlers-Dl5a7JS4.mjs +572 -0
- package/dist/handlers-Dx2d7jt7.mjs +2537 -0
- package/dist/handlers-Dz9PYsCa.mjs +2805 -0
- package/dist/handlers-HujRKC3b.mjs +661 -0
- package/dist/handlers.impl-XWXkQfyi.mjs +807 -0
- package/dist/hooks-B1B8NRHL.mjs +898 -0
- package/dist/index.mjs +491 -259
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-PRMkLVRW.mjs +835 -0
- package/dist/manifest-67Bok-Si.mjs +58 -0
- package/dist/manifest-6lNTMZAB2.mjs +87 -0
- package/dist/manifest-B2duEHiH.mjs +90 -0
- package/dist/manifest-B6EY9Vm8.mjs +57 -0
- package/dist/manifest-B6nKSbyY.mjs +95 -0
- package/dist/manifest-BL8AQNPF.mjs +106 -0
- package/dist/manifest-BSZvJJmV.mjs +47 -0
- package/dist/manifest-BU7qzUyX.mjs +418 -0
- package/dist/manifest-Bl62e8WK.mjs +49 -0
- package/dist/manifest-Bo5cXjdt.mjs +82 -0
- package/dist/manifest-BpS4gtUK.mjs +1347 -0
- package/dist/manifest-Bv65_e2W.mjs +101 -0
- package/dist/manifest-BytNIF4Z.mjs +117 -0
- package/dist/manifest-C-xtsjS3.mjs +81 -0
- package/dist/manifest-CDYl7OhA.mjs +66 -0
- package/dist/manifest-CRZ3xmkD.mjs +61 -0
- package/dist/manifest-CoW6u4Tp.mjs +132 -0
- package/dist/manifest-Cq5zN_8A.mjs +50 -0
- package/dist/manifest-D7YZM_2e.mjs +194 -0
- package/dist/manifest-DE_VrAeQ.mjs +314 -0
- package/dist/manifest-DGsXSCpT.mjs +39 -0
- package/dist/manifest-DJ2vfEuW.mjs +156 -0
- package/dist/manifest-DPXDYhEu.mjs +80 -0
- package/dist/manifest-Dd4fQb0a.mjs +322 -0
- package/dist/manifest-Deq6opGg.mjs +223 -0
- package/dist/manifest-DfJTafJK.mjs +37 -0
- package/dist/manifest-DgOdgN_j.mjs +50 -0
- package/dist/manifest-DlbMW4v4.mjs +47 -0
- package/dist/manifest-DmVfbH0w.mjs +374 -0
- package/dist/manifest-Dog6Ddjr.mjs +109 -0
- package/dist/manifest-DvgU5FWb.mjs +58 -0
- package/dist/manifest-HsfDBs7j.mjs +50 -0
- package/dist/manifest-I8oQHvCG.mjs +186 -0
- package/dist/manifest-NvH_a-av.mjs +786 -0
- package/dist/manifest-cEJU1v0Z.mjs +129 -0
- package/dist/manifest-wOl5XLB12.mjs +112 -0
- package/dist/modules-tZozf0LQ.mjs +10635 -0
- package/dist/mojo-ipc-DXNEXEqb.mjs +640 -0
- package/dist/network-CPVvwvFg.mjs +3852 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-um7lCRY3.mjs} +219 -216
- package/dist/parse-args-B4cY5Vx5.mjs +39 -0
- package/dist/platform-CYeFoTWp.mjs +2161 -0
- package/dist/process-BTbgcVc6.mjs +1306 -0
- package/dist/proxy-r8YN6nP1.mjs +192 -0
- package/dist/registry-Bl8ZQW61.mjs +34 -0
- package/dist/response-CWhh2aLo.mjs +34 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-BoZnSoj-.mjs +586 -0
- package/dist/sourcemap-BIDHUVXy.mjs +934 -0
- package/dist/ssrf-policy-Dsqd-DTX.mjs +166 -0
- package/dist/streaming-Dal6utPp.mjs +725 -0
- package/dist/tool-builder-BHJp32mV.mjs +186 -0
- package/dist/transform-DRVgGG90.mjs +1011 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-BYx5UOeG.mjs +1044 -0
- package/dist/webcrack-Be0_FccV.mjs +747 -0
- package/dist/workflow-BpuKEtvn.mjs +725 -0
- package/package.json +82 -49
- package/dist/ExtensionManager-CPTJhHFg.mjs +0 -2
- package/dist/ToolCatalog-Bq4V2sbJ.mjs +0 -67201
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-jJFy20G-.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-BWmPgQsv.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-K3V4vGsw.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-nnMvEohD.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-DDBWs9UP.mjs} +1 -1
|
@@ -0,0 +1,4369 @@
|
|
|
1
|
+
import { t as logger } from "./logger-Dh_xb7_2.mjs";
|
|
2
|
+
import { C as CAPTCHA_MAX_RETRIES, D as CAPTCHA_RESULT_TIMEOUT_MS, E as CAPTCHA_POLL_INTERVAL_MS, O as CAPTCHA_SOLVER_BASE_URL, S as CAPTCHA_DEFAULT_TIMEOUT_MS, T as CAPTCHA_MIN_TIMEOUT_MS, k as CAPTCHA_SUBMIT_TIMEOUT_MS, w as CAPTCHA_MAX_TIMEOUT_MS, wn as SCRIPTS_MAX_CAP, x as CAPTCHA_DEFAULT_RETRIES } from "./constants-CDZLOoVv.mjs";
|
|
3
|
+
import { t as DetailedDataManager } from "./DetailedDataManager-HT49OrvF.mjs";
|
|
4
|
+
import { c as getConfig, l as projectRoot, o as resolveOutputDirectory, s as resolveScreenshotOutputPath } from "./outputPaths-um7lCRY3.mjs";
|
|
5
|
+
import { c as CamoufoxBrowserManager, n as StealthScripts, s as AICaptchaDetector } from "./modules-tZozf0LQ.mjs";
|
|
6
|
+
import { t as PrerequisiteError } from "./PrerequisiteError-TuyZIs6n.mjs";
|
|
7
|
+
import { n as isBetterSqlite3RelatedError, t as formatBetterSqlite3Error } from "./betterSqlite3-DLSBZodi.mjs";
|
|
8
|
+
import { t as cdpLimit } from "./concurrency-Drev_Vz9.mjs";
|
|
9
|
+
import { a as argString, i as argObject, n as argEnum, o as argStringArray, r as argNumber, s as argStringRequired, t as argBool } from "./parse-args-B4cY5Vx5.mjs";
|
|
10
|
+
import { n as capabilityReport } from "./capabilities-CcHlvWgK.mjs";
|
|
11
|
+
import "./definitions-BRaefg3u.mjs";
|
|
12
|
+
import { t as R } from "./ResponseBuilder-CJXWmWNw.mjs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { readFile, writeFile } from "fs/promises";
|
|
16
|
+
//#region src/server/domains/browser/handlers/browser-control.ts
|
|
17
|
+
const projectEnvPath = join(projectRoot, ".env");
|
|
18
|
+
const CHROME_CHANNELS = new Set([
|
|
19
|
+
"stable",
|
|
20
|
+
"beta",
|
|
21
|
+
"dev",
|
|
22
|
+
"canary"
|
|
23
|
+
]);
|
|
24
|
+
var BrowserControlHandlers = class {
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
}
|
|
28
|
+
markMonitoringContextChanged(context) {
|
|
29
|
+
try {
|
|
30
|
+
this.deps.consoleMonitor.markContextChanged();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.warn(`[${context}] Failed to mark monitoring context as stale: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async syncTabRegistryWithCollectorPages(context) {
|
|
36
|
+
try {
|
|
37
|
+
const resolvedPages = await this.deps.collector.listResolvedPages();
|
|
38
|
+
this.deps.getTabRegistry().reconcilePages(resolvedPages.map((entry) => entry.page), resolvedPages.map(({ page: _page, ...meta }) => meta));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.warn(`[${context}] Failed to sync attached tabs into TabRegistry: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
parseHeadlessArg(value) {
|
|
44
|
+
if (typeof value === "boolean") return value;
|
|
45
|
+
if (typeof value === "number") {
|
|
46
|
+
if (value === 1) return true;
|
|
47
|
+
if (value === 0) return false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
const normalized = value.trim().toLowerCase();
|
|
52
|
+
if ([
|
|
53
|
+
"true",
|
|
54
|
+
"1",
|
|
55
|
+
"yes",
|
|
56
|
+
"on"
|
|
57
|
+
].includes(normalized)) return true;
|
|
58
|
+
if ([
|
|
59
|
+
"false",
|
|
60
|
+
"0",
|
|
61
|
+
"no",
|
|
62
|
+
"off"
|
|
63
|
+
].includes(normalized)) return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
parseChromeConnectRequest(args) {
|
|
67
|
+
const channelValue = argString(args, "channel");
|
|
68
|
+
if (channelValue && !CHROME_CHANNELS.has(channelValue)) throw new Error(`Invalid channel "${channelValue}". Expected one of: stable, beta, dev, canary.`);
|
|
69
|
+
return {
|
|
70
|
+
browserURL: argString(args, "browserURL"),
|
|
71
|
+
wsEndpoint: argString(args, "wsEndpoint"),
|
|
72
|
+
autoConnect: argBool(args, "autoConnect"),
|
|
73
|
+
userDataDir: argString(args, "userDataDir"),
|
|
74
|
+
channel: channelValue
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
parseChromeLaunchRequest(args) {
|
|
78
|
+
return {
|
|
79
|
+
headless: this.parseHeadlessArg(args.headless),
|
|
80
|
+
args: argStringArray(args, "args"),
|
|
81
|
+
enableV8NativesSyntax: argBool(args, "enableV8NativesSyntax")
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
hasChromeConnectRequest(request) {
|
|
85
|
+
return Boolean(request.browserURL || request.wsEndpoint || request.autoConnect || request.userDataDir || request.channel);
|
|
86
|
+
}
|
|
87
|
+
describeChromeConnectRequest(request) {
|
|
88
|
+
if (request.wsEndpoint) return request.wsEndpoint;
|
|
89
|
+
if (request.browserURL) return request.browserURL;
|
|
90
|
+
if (request.userDataDir) return `autoConnect:${request.userDataDir}`;
|
|
91
|
+
return `autoConnect:${request.channel ?? "stable"}`;
|
|
92
|
+
}
|
|
93
|
+
isAutoConnectRequest(request) {
|
|
94
|
+
return Boolean(request.autoConnect || request.userDataDir || request.channel);
|
|
95
|
+
}
|
|
96
|
+
getAutoConnectApprovalHint(request) {
|
|
97
|
+
if (!this.isAutoConnectRequest(request)) return null;
|
|
98
|
+
return "Chrome 144+ autoConnect may prompt for manual approval. Switch to Chrome and click Allow for this client if prompted.";
|
|
99
|
+
}
|
|
100
|
+
shouldAttemptLinuxHeadfulFallback(headlessArg, error) {
|
|
101
|
+
const requestedHeadful = headlessArg === false || headlessArg === void 0 && process.env.PUPPETEER_HEADLESS === "false";
|
|
102
|
+
const linuxRuntime = process.platform === "linux" || process.env.JSHOOK_FORCE_LINUX_FALLBACK === "true";
|
|
103
|
+
if (!requestedHeadful || !linuxRuntime) return false;
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
return /Missing X server|cannot open display|Failed to launch the browser process|ozone|No protocol specified|X11|Wayland|DevToolsActivePort/i.test(message);
|
|
106
|
+
}
|
|
107
|
+
async persistHeadlessEnv(value) {
|
|
108
|
+
try {
|
|
109
|
+
let envContent = "";
|
|
110
|
+
try {
|
|
111
|
+
envContent = await readFile(projectEnvPath, "utf-8");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error?.code !== "ENOENT") throw error;
|
|
114
|
+
}
|
|
115
|
+
const nextLine = `PUPPETEER_HEADLESS=${value}`;
|
|
116
|
+
await writeFile(projectEnvPath, /^PUPPETEER_HEADLESS=.*$/m.test(envContent) ? envContent.replace(/^PUPPETEER_HEADLESS=.*$/m, nextLine) : `${envContent.trimEnd()}\n${nextLine}\n`, "utf-8");
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.warn(`Failed to persist PUPPETEER_HEADLESS=${value} to .env: ${String(error)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async handleBrowserLaunch(args) {
|
|
122
|
+
try {
|
|
123
|
+
if (argString(args, "driver", "chrome") === "camoufox") {
|
|
124
|
+
if (argString(args, "mode", "launch") === "connect") {
|
|
125
|
+
const wsEndpoint = argString(args, "wsEndpoint");
|
|
126
|
+
if (!wsEndpoint) return R.fail("wsEndpoint is required for connect mode. Use camoufox_server({ action: \"launch\" }) first to get a wsEndpoint.").json();
|
|
127
|
+
return R.ok().merge({
|
|
128
|
+
driver: "camoufox",
|
|
129
|
+
mode: "connect",
|
|
130
|
+
wsEndpoint,
|
|
131
|
+
message: "Connected to Camoufox server. Use page_navigate to begin."
|
|
132
|
+
}).json();
|
|
133
|
+
}
|
|
134
|
+
return R.ok().merge({
|
|
135
|
+
driver: "camoufox",
|
|
136
|
+
mode: "launch",
|
|
137
|
+
message: "Camoufox (Firefox) browser launched",
|
|
138
|
+
note: "Use page_navigate to begin. CDP debugger is limited in Firefox; network_enable and console_monitor({ action: \"enable\" }) use Playwright events and are fully supported."
|
|
139
|
+
}).json();
|
|
140
|
+
}
|
|
141
|
+
if (argString(args, "mode", "launch") === "connect") {
|
|
142
|
+
const connectRequest = this.parseChromeConnectRequest(args);
|
|
143
|
+
if (!this.hasChromeConnectRequest(connectRequest)) return R.fail("browserURL, wsEndpoint, autoConnect, userDataDir, or channel is required for chrome connect mode.").json();
|
|
144
|
+
await this.deps.collector.connect(connectRequest);
|
|
145
|
+
const status = await this.deps.collector.getStatus();
|
|
146
|
+
return R.ok().merge({
|
|
147
|
+
driver: "chrome",
|
|
148
|
+
mode: "connect",
|
|
149
|
+
endpoint: this.describeChromeConnectRequest(connectRequest),
|
|
150
|
+
autoConnect: this.isAutoConnectRequest(connectRequest),
|
|
151
|
+
channel: connectRequest.channel ?? null,
|
|
152
|
+
userDataDir: connectRequest.userDataDir ?? null,
|
|
153
|
+
manualApprovalMayBeRequired: this.isAutoConnectRequest(connectRequest),
|
|
154
|
+
approvalHint: this.getAutoConnectApprovalHint(connectRequest),
|
|
155
|
+
message: "Connected to existing Chrome browser successfully",
|
|
156
|
+
status
|
|
157
|
+
}).json();
|
|
158
|
+
}
|
|
159
|
+
const launchRequest = this.parseChromeLaunchRequest(args);
|
|
160
|
+
try {
|
|
161
|
+
const launch = await this.deps.collector.launch(launchRequest);
|
|
162
|
+
if (launch.action === "relaunched") this.markMonitoringContextChanged("browser_launch_relaunch");
|
|
163
|
+
const status = await this.deps.collector.getStatus();
|
|
164
|
+
return R.ok().merge({
|
|
165
|
+
driver: "chrome",
|
|
166
|
+
message: launch.action === "relaunched" ? "Browser relaunched successfully" : "Browser launched successfully",
|
|
167
|
+
launchAction: launch.action,
|
|
168
|
+
relaunchReason: launch.reason ?? null,
|
|
169
|
+
v8NativeSyntaxEnabled: launch.launchOptions.v8NativeSyntaxEnabled,
|
|
170
|
+
launchArgs: launch.launchOptions.args,
|
|
171
|
+
status
|
|
172
|
+
}).json();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (!this.shouldAttemptLinuxHeadfulFallback(launchRequest.headless, error)) throw error;
|
|
175
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
176
|
+
logger.warn(`Headful launch failed on Linux, fallback to headless=true: ${reason}`);
|
|
177
|
+
process.env.PUPPETEER_HEADLESS = "true";
|
|
178
|
+
await this.persistHeadlessEnv("true");
|
|
179
|
+
const launch = await this.deps.collector.launch({
|
|
180
|
+
...launchRequest,
|
|
181
|
+
headless: true
|
|
182
|
+
});
|
|
183
|
+
const fallbackStatus = await this.deps.collector.getStatus();
|
|
184
|
+
return R.ok().merge({
|
|
185
|
+
driver: "chrome",
|
|
186
|
+
message: "Browser launched with Linux fallback (headless=true)",
|
|
187
|
+
launchAction: launch.action,
|
|
188
|
+
relaunchReason: launch.reason ?? null,
|
|
189
|
+
v8NativeSyntaxEnabled: launch.launchOptions.v8NativeSyntaxEnabled,
|
|
190
|
+
launchArgs: launch.launchOptions.args,
|
|
191
|
+
status: fallbackStatus,
|
|
192
|
+
fallback: {
|
|
193
|
+
applied: true,
|
|
194
|
+
reason: "Headful browser is unavailable in current Linux runtime; switched to headless and updated .env",
|
|
195
|
+
newEnv: "PUPPETEER_HEADLESS=true"
|
|
196
|
+
}
|
|
197
|
+
}).json();
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return R.fail(error).json();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async handleBrowserClose(_args) {
|
|
204
|
+
try {
|
|
205
|
+
await this.deps.collector.close();
|
|
206
|
+
return R.ok().set("message", "Browser closed successfully").json();
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return R.fail(e).json();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async handleBrowserStatus(_args) {
|
|
212
|
+
try {
|
|
213
|
+
const status = await this.deps.collector.getStatus();
|
|
214
|
+
return R.ok().merge({
|
|
215
|
+
driver: "chrome",
|
|
216
|
+
...status
|
|
217
|
+
}).json();
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return R.fail(e).json();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async handleBrowserListTabs(args) {
|
|
223
|
+
const connectRequest = this.parseChromeConnectRequest(args);
|
|
224
|
+
try {
|
|
225
|
+
if (this.hasChromeConnectRequest(connectRequest)) await this.deps.collector.connect(connectRequest);
|
|
226
|
+
const pages = await this.deps.collector.listPages();
|
|
227
|
+
const registry = this.deps.getTabRegistry();
|
|
228
|
+
await this.syncTabRegistryWithCollectorPages("browser_list_tabs");
|
|
229
|
+
const enrichedPages = pages.map((page) => {
|
|
230
|
+
const tab = registry.getTabByIndex(page.index);
|
|
231
|
+
return {
|
|
232
|
+
...page,
|
|
233
|
+
pageId: tab?.pageId ?? null,
|
|
234
|
+
aliases: tab?.aliases ?? []
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
const currentInfo = registry.getContextMeta();
|
|
238
|
+
return R.ok().merge({
|
|
239
|
+
count: pages.length,
|
|
240
|
+
pages: enrichedPages,
|
|
241
|
+
currentPageId: currentInfo.pageId,
|
|
242
|
+
currentIndex: currentInfo.tabIndex,
|
|
243
|
+
autoConnect: this.isAutoConnectRequest(connectRequest),
|
|
244
|
+
manualApprovalMayBeRequired: this.isAutoConnectRequest(connectRequest),
|
|
245
|
+
approvalHint: this.getAutoConnectApprovalHint(connectRequest),
|
|
246
|
+
hint: "Use browser_select_tab(index=N) to switch to a specific tab"
|
|
247
|
+
}).json();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return R.fail(error).set("hint", "Make sure browser is attached via browser_attach first, or provide browserURL/autoConnect. Chrome 144+ autoConnect may require manual approval in the Chrome window.").set("approvalHint", this.getAutoConnectApprovalHint(connectRequest)).json();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async handleBrowserSelectTab(args) {
|
|
253
|
+
try {
|
|
254
|
+
const index = argNumber(args, "index");
|
|
255
|
+
const urlPattern = argString(args, "urlPattern");
|
|
256
|
+
const titlePattern = argString(args, "titlePattern");
|
|
257
|
+
const registry = this.deps.getTabRegistry();
|
|
258
|
+
if (index !== void 0) {
|
|
259
|
+
const clearedTarget = await this.deps.clearAttachedTargetContext("browser_select_tab");
|
|
260
|
+
await this.deps.collector.selectPage(index);
|
|
261
|
+
const pages = await this.deps.collector.listPages();
|
|
262
|
+
await this.syncTabRegistryWithCollectorPages("browser_select_tab");
|
|
263
|
+
const selected = pages[index];
|
|
264
|
+
const tab = registry.setCurrentByIndex(index);
|
|
265
|
+
if (tab?.pageId || clearedTarget.detached) this.markMonitoringContextChanged("browser_select_tab");
|
|
266
|
+
return R.ok().merge({
|
|
267
|
+
selectedIndex: index,
|
|
268
|
+
selectedPageId: tab?.pageId ?? null,
|
|
269
|
+
url: selected?.url,
|
|
270
|
+
title: selected?.title,
|
|
271
|
+
contextSwitched: true,
|
|
272
|
+
detachedCdpTarget: clearedTarget.detached,
|
|
273
|
+
detachedCdpTargetId: clearedTarget.targetId,
|
|
274
|
+
monitoringBindingDeferred: Boolean(tab?.pageId),
|
|
275
|
+
networkMonitoringEnabled: false,
|
|
276
|
+
consoleMonitoringEnabled: false
|
|
277
|
+
}).json();
|
|
278
|
+
}
|
|
279
|
+
const pages = await this.deps.collector.listPages();
|
|
280
|
+
let matchIndex = -1;
|
|
281
|
+
for (const page of pages) {
|
|
282
|
+
if (urlPattern && page.url.includes(urlPattern)) {
|
|
283
|
+
matchIndex = page.index;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (titlePattern && page.title.includes(titlePattern)) {
|
|
287
|
+
matchIndex = page.index;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (matchIndex === -1) return R.fail("No matching tab found").set("availablePages", pages).json();
|
|
292
|
+
const clearedTarget = await this.deps.clearAttachedTargetContext("browser_select_tab");
|
|
293
|
+
await this.deps.collector.selectPage(matchIndex);
|
|
294
|
+
await this.syncTabRegistryWithCollectorPages("browser_select_tab");
|
|
295
|
+
const selected = pages[matchIndex];
|
|
296
|
+
const tab = registry.setCurrentByIndex(matchIndex);
|
|
297
|
+
if (tab?.pageId || clearedTarget.detached) this.markMonitoringContextChanged("browser_select_tab");
|
|
298
|
+
return R.ok().merge({
|
|
299
|
+
selectedIndex: matchIndex,
|
|
300
|
+
selectedPageId: tab?.pageId ?? null,
|
|
301
|
+
url: selected?.url,
|
|
302
|
+
title: selected?.title,
|
|
303
|
+
contextSwitched: true,
|
|
304
|
+
detachedCdpTarget: clearedTarget.detached,
|
|
305
|
+
detachedCdpTargetId: clearedTarget.targetId,
|
|
306
|
+
monitoringBindingDeferred: Boolean(tab?.pageId),
|
|
307
|
+
networkMonitoringEnabled: false,
|
|
308
|
+
consoleMonitoringEnabled: false
|
|
309
|
+
}).json();
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return R.fail(error).json();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async handleBrowserAttach(args) {
|
|
315
|
+
let connectRequest = null;
|
|
316
|
+
try {
|
|
317
|
+
connectRequest = this.parseChromeConnectRequest(args);
|
|
318
|
+
if (!this.hasChromeConnectRequest(connectRequest)) return R.fail("browserURL, wsEndpoint, autoConnect, userDataDir, or channel is required").json();
|
|
319
|
+
const clearedTarget = await this.deps.clearAttachedTargetContext("browser_attach");
|
|
320
|
+
await this.deps.collector.connect(connectRequest);
|
|
321
|
+
const rawPageIndex = args.pageIndex;
|
|
322
|
+
const pageIndex = typeof rawPageIndex === "number" ? rawPageIndex : typeof rawPageIndex === "string" && rawPageIndex.trim() !== "" ? Number(rawPageIndex) : 0;
|
|
323
|
+
const selectedIndex = Number.isFinite(pageIndex) ? pageIndex : 0;
|
|
324
|
+
const pages = await this.deps.collector.listPages();
|
|
325
|
+
if (pages.length > 0 && selectedIndex < pages.length) await this.deps.collector.selectPage(selectedIndex);
|
|
326
|
+
else if (pages.length > 0) {
|
|
327
|
+
await this.deps.collector.selectPage(0);
|
|
328
|
+
logger.warn(`[browser_attach] pageIndex ${selectedIndex} out of range (0-${pages.length - 1}), fell back to 0`);
|
|
329
|
+
}
|
|
330
|
+
const registry = this.deps.getTabRegistry();
|
|
331
|
+
await this.syncTabRegistryWithCollectorPages("browser_attach");
|
|
332
|
+
const actualIndex = pages.length > 0 ? Math.min(selectedIndex, pages.length - 1) : 0;
|
|
333
|
+
const tab = pages.length > 0 ? registry.setCurrentByIndex(actualIndex) : null;
|
|
334
|
+
const selected = pages[actualIndex];
|
|
335
|
+
const pageHandleReady = Boolean(tab?.pageId);
|
|
336
|
+
if (pageHandleReady) this.markMonitoringContextChanged("browser_attach");
|
|
337
|
+
const status = await this.deps.collector.getStatus();
|
|
338
|
+
return R.ok().merge({
|
|
339
|
+
message: "Attached to existing browser successfully",
|
|
340
|
+
endpoint: this.describeChromeConnectRequest(connectRequest),
|
|
341
|
+
autoConnect: this.isAutoConnectRequest(connectRequest),
|
|
342
|
+
channel: connectRequest.channel ?? null,
|
|
343
|
+
userDataDir: connectRequest.userDataDir ?? null,
|
|
344
|
+
manualApprovalMayBeRequired: this.isAutoConnectRequest(connectRequest),
|
|
345
|
+
approvalHint: this.getAutoConnectApprovalHint(connectRequest),
|
|
346
|
+
selectedIndex: actualIndex,
|
|
347
|
+
selectedPageId: tab?.pageId ?? null,
|
|
348
|
+
currentUrl: selected?.url ?? null,
|
|
349
|
+
currentTitle: selected?.title ?? null,
|
|
350
|
+
totalPages: pages.length,
|
|
351
|
+
contextSwitched: pages.length > 0,
|
|
352
|
+
detachedCdpTarget: clearedTarget.detached,
|
|
353
|
+
detachedCdpTargetId: clearedTarget.targetId,
|
|
354
|
+
monitoringBindingDeferred: pageHandleReady,
|
|
355
|
+
networkMonitoringEnabled: false,
|
|
356
|
+
consoleMonitoringEnabled: false,
|
|
357
|
+
takeoverReady: pageHandleReady,
|
|
358
|
+
note: pageHandleReady ? "Monitoring will auto-rebind on the next console/network operation for the selected tab." : "Connected to existing Chrome, but the selected tab does not currently expose a stable Puppeteer Page handle. Tab discovery still works; try selecting a different tab or navigate the tab and retry.",
|
|
359
|
+
status
|
|
360
|
+
}).json();
|
|
361
|
+
} catch (error) {
|
|
362
|
+
return R.fail(error).set("approvalHint", this.getAutoConnectApprovalHint(connectRequest ?? {})).json();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/server/domains/browser/handlers/camoufox-browser.ts
|
|
368
|
+
function extractCamoufoxServerConfig(args) {
|
|
369
|
+
const addons = argStringArray(args, "addons");
|
|
370
|
+
const excludeAddons = argStringArray(args, "excludeAddons");
|
|
371
|
+
const fonts = argStringArray(args, "fonts");
|
|
372
|
+
return {
|
|
373
|
+
headless: argBool(args, "headless", true),
|
|
374
|
+
os: argString(args, "os", "windows"),
|
|
375
|
+
geoip: argBool(args, "geoip", false),
|
|
376
|
+
humanize: argBool(args, "humanize", false),
|
|
377
|
+
proxy: argString(args, "proxy") || void 0,
|
|
378
|
+
blockImages: argBool(args, "blockImages", false),
|
|
379
|
+
blockWebrtc: argBool(args, "blockWebrtc", false),
|
|
380
|
+
blockWebgl: argBool(args, "blockWebgl", false),
|
|
381
|
+
locale: argString(args, "locale") || void 0,
|
|
382
|
+
addons: addons.length > 0 ? addons : void 0,
|
|
383
|
+
fonts: fonts.length > 0 ? fonts : void 0,
|
|
384
|
+
excludeAddons: excludeAddons.length > 0 ? excludeAddons : void 0,
|
|
385
|
+
customFontsOnly: argBool(args, "customFontsOnly", false),
|
|
386
|
+
screen: args.screen,
|
|
387
|
+
window: args.window,
|
|
388
|
+
fingerprint: argObject(args, "fingerprint"),
|
|
389
|
+
webglConfig: argObject(args, "webglConfig"),
|
|
390
|
+
firefoxUserPrefs: argObject(args, "firefoxUserPrefs"),
|
|
391
|
+
mainWorldEval: argBool(args, "mainWorldEval", false),
|
|
392
|
+
enableCache: argBool(args, "enableCache", false)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Check if camoufox-js is available and has all required dependencies.
|
|
397
|
+
* Returns error message if not available, null if available.
|
|
398
|
+
*/
|
|
399
|
+
async function checkCamoufoxDependencies() {
|
|
400
|
+
try {
|
|
401
|
+
await import("camoufox-js");
|
|
402
|
+
return null;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
405
|
+
if (isBetterSqlite3RelatedError(error)) return `Camoufox requires the same native SQLite backend used by trace tooling. ${formatBetterSqlite3Error(error)}`;
|
|
406
|
+
if (errorMsg.includes("Cannot find package 'camoufox-js'")) return "camoufox-js package is not installed. Run: pnpm add camoufox-js && npx camoufox-js fetch";
|
|
407
|
+
return `Camoufox dependencies check failed: ${errorMsg}`;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
var CamoufoxBrowserHandlers = class {
|
|
411
|
+
constructor(deps) {
|
|
412
|
+
this.deps = deps;
|
|
413
|
+
}
|
|
414
|
+
async handleCamoufoxServerLaunch(args) {
|
|
415
|
+
try {
|
|
416
|
+
const depError = await checkCamoufoxDependencies();
|
|
417
|
+
if (depError) {
|
|
418
|
+
logger.warn(`Camoufox dependencies not available: ${depError}`);
|
|
419
|
+
return R.fail(depError).set("hint", "Camoufox is optional. Use browser_launch with Chrome driver instead, or install dependencies.").json();
|
|
420
|
+
}
|
|
421
|
+
const port = argNumber(args, "port");
|
|
422
|
+
const ws_path = argString(args, "ws_path");
|
|
423
|
+
const config = extractCamoufoxServerConfig(args);
|
|
424
|
+
let camoufoxManager = this.deps.getCamoufoxManager();
|
|
425
|
+
if (!camoufoxManager) {
|
|
426
|
+
camoufoxManager = new CamoufoxBrowserManager(config);
|
|
427
|
+
this.deps.setCamoufoxManager(camoufoxManager);
|
|
428
|
+
}
|
|
429
|
+
const wsEndpoint = await camoufoxManager.launchAsServer(port, ws_path);
|
|
430
|
+
return R.ok().merge({
|
|
431
|
+
wsEndpoint,
|
|
432
|
+
config: {
|
|
433
|
+
os: config.os,
|
|
434
|
+
headless: config.headless,
|
|
435
|
+
geoip: config.geoip,
|
|
436
|
+
locale: config.locale,
|
|
437
|
+
blockWebgl: config.blockWebgl
|
|
438
|
+
},
|
|
439
|
+
message: "Camoufox server launched. Connect with: browser_launch(driver=\"camoufox\", mode=\"connect\", wsEndpoint=<wsEndpoint>)"
|
|
440
|
+
}).json();
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return R.fail(error).set("hint", "Try running: npx camoufox-js fetch to download browser binaries").json();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async handleCamoufoxServerClose(_args) {
|
|
446
|
+
try {
|
|
447
|
+
const camoufoxManager = this.deps.getCamoufoxManager();
|
|
448
|
+
if (!camoufoxManager) return R.fail("No camoufox server is running.").json();
|
|
449
|
+
await camoufoxManager.closeBrowserServer();
|
|
450
|
+
return R.ok().set("message", "Camoufox server closed.").json();
|
|
451
|
+
} catch (e) {
|
|
452
|
+
return R.fail(e).json();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async handleCamoufoxServerStatus(_args) {
|
|
456
|
+
try {
|
|
457
|
+
const wsEndpoint = this.deps.getCamoufoxManager()?.getBrowserServerEndpoint() ?? null;
|
|
458
|
+
return R.ok().merge({
|
|
459
|
+
running: wsEndpoint !== null,
|
|
460
|
+
wsEndpoint
|
|
461
|
+
}).json();
|
|
462
|
+
} catch (e) {
|
|
463
|
+
return R.fail(e).json();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/server/domains/browser/handlers/page-navigation.ts
|
|
469
|
+
var PageNavigationHandlers = class {
|
|
470
|
+
constructor(deps) {
|
|
471
|
+
this.deps = deps;
|
|
472
|
+
}
|
|
473
|
+
async handlePageNavigate(args) {
|
|
474
|
+
try {
|
|
475
|
+
const url = argString(args, "url", "");
|
|
476
|
+
const rawWaitUntil = argString(args, "waitUntil", "networkidle");
|
|
477
|
+
const timeout = argNumber(args, "timeout");
|
|
478
|
+
const enableNetworkMonitoring = argBool(args, "enableNetworkMonitoring");
|
|
479
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
480
|
+
const playwrightWaitUntil = rawWaitUntil === "networkidle2" ? "networkidle" : rawWaitUntil;
|
|
481
|
+
const page = await this.deps.getCamoufoxPage();
|
|
482
|
+
await page.goto(url, {
|
|
483
|
+
waitUntil: playwrightWaitUntil,
|
|
484
|
+
timeout
|
|
485
|
+
});
|
|
486
|
+
this.deps.consoleMonitor.setPlaywrightPage(page);
|
|
487
|
+
if (enableNetworkMonitoring) await this.deps.consoleMonitor.enable({
|
|
488
|
+
enableNetwork: true,
|
|
489
|
+
enableExceptions: true
|
|
490
|
+
});
|
|
491
|
+
const navigatedUrl = page.url();
|
|
492
|
+
this.deps.eventBus?.emit("browser:navigated", {
|
|
493
|
+
url: navigatedUrl,
|
|
494
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
495
|
+
});
|
|
496
|
+
return R.ok().build({
|
|
497
|
+
driver: "camoufox",
|
|
498
|
+
url: navigatedUrl,
|
|
499
|
+
title: await page.title(),
|
|
500
|
+
network_monitoring: { enabled: this.deps.consoleMonitor.isNetworkEnabled() }
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (enableNetworkMonitoring) await this.deps.consoleMonitor.enable({
|
|
504
|
+
enableNetwork: true,
|
|
505
|
+
enableExceptions: true
|
|
506
|
+
});
|
|
507
|
+
const waitUntil = {
|
|
508
|
+
networkidle: "networkidle2",
|
|
509
|
+
commit: "load"
|
|
510
|
+
}[rawWaitUntil] || rawWaitUntil;
|
|
511
|
+
await this.deps.pageController.navigate(url, {
|
|
512
|
+
waitUntil,
|
|
513
|
+
timeout
|
|
514
|
+
});
|
|
515
|
+
const currentUrl = await this.deps.pageController.getURL();
|
|
516
|
+
const title = await this.deps.pageController.getTitle();
|
|
517
|
+
this.deps.eventBus?.emit("browser:navigated", {
|
|
518
|
+
url: currentUrl,
|
|
519
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
520
|
+
});
|
|
521
|
+
return R.ok().build({
|
|
522
|
+
url: currentUrl,
|
|
523
|
+
title,
|
|
524
|
+
network_monitoring: { enabled: this.deps.consoleMonitor.isNetworkEnabled() }
|
|
525
|
+
});
|
|
526
|
+
} catch (e) {
|
|
527
|
+
return R.fail(e).build();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async handlePageReload(_args) {
|
|
531
|
+
try {
|
|
532
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
533
|
+
await (await this.deps.getCamoufoxPage()).reload();
|
|
534
|
+
return R.ok().build({
|
|
535
|
+
message: "Page reloaded",
|
|
536
|
+
driver: "camoufox"
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
await this.deps.pageController.reload();
|
|
540
|
+
return R.ok().build({ message: "Page reloaded" });
|
|
541
|
+
} catch (e) {
|
|
542
|
+
return R.fail(e).build();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async handlePageBack(args) {
|
|
546
|
+
try {
|
|
547
|
+
const timeout = argNumber(args, "timeout", 1e4);
|
|
548
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
549
|
+
const page = await this.deps.getCamoufoxPage();
|
|
550
|
+
await page.goBack();
|
|
551
|
+
return R.ok().build({
|
|
552
|
+
url: page.url(),
|
|
553
|
+
driver: "camoufox"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
await this.deps.pageController.goBack(timeout);
|
|
557
|
+
const url = await this.deps.pageController.getURL();
|
|
558
|
+
return R.ok().build({ url });
|
|
559
|
+
} catch (e) {
|
|
560
|
+
return R.fail(e).build();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async handlePageForward(args) {
|
|
564
|
+
try {
|
|
565
|
+
const timeout = argNumber(args, "timeout", 1e4);
|
|
566
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
567
|
+
const page = await this.deps.getCamoufoxPage();
|
|
568
|
+
await page.goForward();
|
|
569
|
+
return R.ok().build({
|
|
570
|
+
url: page.url(),
|
|
571
|
+
driver: "camoufox"
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
await this.deps.pageController.goForward(timeout);
|
|
575
|
+
const url = await this.deps.pageController.getURL();
|
|
576
|
+
return R.ok().build({ url });
|
|
577
|
+
} catch (e) {
|
|
578
|
+
return R.fail(e).build();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/server/domains/browser/handlers/page-interaction.ts
|
|
584
|
+
var PageInteractionHandlers = class {
|
|
585
|
+
constructor(deps) {
|
|
586
|
+
this.deps = deps;
|
|
587
|
+
}
|
|
588
|
+
toErrorMessage(error) {
|
|
589
|
+
if (error instanceof Error) return error.message;
|
|
590
|
+
return typeof error === "string" ? error : "";
|
|
591
|
+
}
|
|
592
|
+
parseNumberArg(value, options = {}) {
|
|
593
|
+
let parsed;
|
|
594
|
+
if (typeof value === "number" && Number.isFinite(value)) parsed = value;
|
|
595
|
+
else if (typeof value === "string") {
|
|
596
|
+
const trimmed = value.trim();
|
|
597
|
+
if (trimmed.length > 0) {
|
|
598
|
+
const n = Number(trimmed);
|
|
599
|
+
if (Number.isFinite(n)) parsed = n;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (parsed === void 0) parsed = options.defaultValue;
|
|
603
|
+
if (parsed === void 0) return;
|
|
604
|
+
if (options.integer) parsed = Math.trunc(parsed);
|
|
605
|
+
if (typeof options.min === "number") parsed = Math.max(options.min, parsed);
|
|
606
|
+
if (typeof options.max === "number") parsed = Math.min(options.max, parsed);
|
|
607
|
+
return parsed;
|
|
608
|
+
}
|
|
609
|
+
parseMouseButton(value) {
|
|
610
|
+
if (typeof value === "string") {
|
|
611
|
+
const normalized = value.trim().toLowerCase();
|
|
612
|
+
if (normalized === "left" || normalized === "right" || normalized === "middle") return normalized;
|
|
613
|
+
}
|
|
614
|
+
return "left";
|
|
615
|
+
}
|
|
616
|
+
async getCamoufoxInteractionContext(frameOptions) {
|
|
617
|
+
const page = await this.deps.getCamoufoxPage();
|
|
618
|
+
if (!frameOptions?.frameUrl && !frameOptions?.frameSelector) return page;
|
|
619
|
+
return await this.deps.pageController.resolveFrame(page, frameOptions);
|
|
620
|
+
}
|
|
621
|
+
async handlePageClick(args) {
|
|
622
|
+
try {
|
|
623
|
+
const selector = argString(args, "selector", "");
|
|
624
|
+
const button = this.parseMouseButton(args.button);
|
|
625
|
+
const clickCount = this.parseNumberArg(args.clickCount, {
|
|
626
|
+
defaultValue: 1,
|
|
627
|
+
min: 1,
|
|
628
|
+
max: 10,
|
|
629
|
+
integer: true
|
|
630
|
+
});
|
|
631
|
+
const delay = this.parseNumberArg(args.delay, {
|
|
632
|
+
min: 0,
|
|
633
|
+
max: 6e4,
|
|
634
|
+
integer: true
|
|
635
|
+
});
|
|
636
|
+
const timeout = this.parseNumberArg(args.timeout, {
|
|
637
|
+
defaultValue: 1e4,
|
|
638
|
+
min: 1e3,
|
|
639
|
+
max: 12e4,
|
|
640
|
+
integer: true
|
|
641
|
+
});
|
|
642
|
+
const frameUrl = argString(args, "frameUrl");
|
|
643
|
+
const frameSelector = argString(args, "frameSelector");
|
|
644
|
+
const frameOptions = frameUrl || frameSelector ? {
|
|
645
|
+
frameUrl: frameUrl || void 0,
|
|
646
|
+
frameSelector: frameSelector || void 0
|
|
647
|
+
} : void 0;
|
|
648
|
+
if (!selector || typeof selector !== "string" || selector.trim().length === 0) return R.fail("selector parameter is required").build();
|
|
649
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
650
|
+
await (await this.getCamoufoxInteractionContext(frameOptions)).click(selector, {
|
|
651
|
+
button,
|
|
652
|
+
clickCount,
|
|
653
|
+
delay
|
|
654
|
+
});
|
|
655
|
+
return R.ok().build({
|
|
656
|
+
driver: "camoufox",
|
|
657
|
+
message: `Clicked: ${selector}`,
|
|
658
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
if (frameOptions) await this.deps.pageController.click(selector, {
|
|
663
|
+
button,
|
|
664
|
+
clickCount,
|
|
665
|
+
delay,
|
|
666
|
+
timeout
|
|
667
|
+
}, frameOptions);
|
|
668
|
+
else await this.deps.pageController.click(selector, {
|
|
669
|
+
button,
|
|
670
|
+
clickCount,
|
|
671
|
+
delay,
|
|
672
|
+
timeout
|
|
673
|
+
});
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const msg = this.toErrorMessage(error);
|
|
676
|
+
if (msg.includes("detached") || msg.includes("timed out") || msg.includes("Execution context was destroyed") || msg.includes("callFunctionOn") || msg.includes("Target closed")) return R.ok().build({
|
|
677
|
+
message: `Clicked ${selector} - navigation triggered`,
|
|
678
|
+
navigated: true,
|
|
679
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
680
|
+
});
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
return R.ok().build({
|
|
684
|
+
message: `Clicked: ${selector}`,
|
|
685
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
686
|
+
});
|
|
687
|
+
} catch (e) {
|
|
688
|
+
return R.fail(e).build();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async handlePageType(args) {
|
|
692
|
+
try {
|
|
693
|
+
const selector = argString(args, "selector", "");
|
|
694
|
+
const text = argString(args, "text", "");
|
|
695
|
+
const delay = argNumber(args, "delay");
|
|
696
|
+
const frameUrl = argString(args, "frameUrl");
|
|
697
|
+
const frameSelector = argString(args, "frameSelector");
|
|
698
|
+
const frameOptions = frameUrl || frameSelector ? {
|
|
699
|
+
frameUrl: frameUrl || void 0,
|
|
700
|
+
frameSelector: frameSelector || void 0
|
|
701
|
+
} : void 0;
|
|
702
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
703
|
+
await (await this.getCamoufoxInteractionContext(frameOptions)).fill(selector, text);
|
|
704
|
+
return R.ok().build({
|
|
705
|
+
driver: "camoufox",
|
|
706
|
+
message: `Typed into ${selector}`,
|
|
707
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (frameOptions) await this.deps.pageController.type(selector, text, { delay }, frameOptions);
|
|
711
|
+
else await this.deps.pageController.type(selector, text, { delay });
|
|
712
|
+
return R.ok().build({
|
|
713
|
+
message: `Typed into ${selector}`,
|
|
714
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
715
|
+
});
|
|
716
|
+
} catch (e) {
|
|
717
|
+
return R.fail(e).build();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async handlePageSelect(args) {
|
|
721
|
+
try {
|
|
722
|
+
const selector = argString(args, "selector", "");
|
|
723
|
+
const values = argStringArray(args, "values");
|
|
724
|
+
const frameUrl = argString(args, "frameUrl");
|
|
725
|
+
const frameSelector = argString(args, "frameSelector");
|
|
726
|
+
const frameOptions = frameUrl || frameSelector ? {
|
|
727
|
+
frameUrl: frameUrl || void 0,
|
|
728
|
+
frameSelector: frameSelector || void 0
|
|
729
|
+
} : void 0;
|
|
730
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
731
|
+
await (await this.getCamoufoxInteractionContext(frameOptions)).selectOption(selector, values);
|
|
732
|
+
return R.ok().build({
|
|
733
|
+
driver: "camoufox",
|
|
734
|
+
message: `Selected in ${selector}: ${values.join(", ")}`,
|
|
735
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (frameOptions) await this.deps.pageController.select(selector, values, frameOptions);
|
|
739
|
+
else await this.deps.pageController.select(selector, values);
|
|
740
|
+
return R.ok().build({
|
|
741
|
+
message: `Selected in ${selector}: ${values.join(", ")}`,
|
|
742
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
743
|
+
});
|
|
744
|
+
} catch (e) {
|
|
745
|
+
return R.fail(e).build();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async handlePageHover(args) {
|
|
749
|
+
try {
|
|
750
|
+
const selector = argString(args, "selector", "");
|
|
751
|
+
const frameUrl = argString(args, "frameUrl");
|
|
752
|
+
const frameSelector = argString(args, "frameSelector");
|
|
753
|
+
const frameOptions = frameUrl || frameSelector ? {
|
|
754
|
+
frameUrl: frameUrl || void 0,
|
|
755
|
+
frameSelector: frameSelector || void 0
|
|
756
|
+
} : void 0;
|
|
757
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
758
|
+
await (await this.getCamoufoxInteractionContext(frameOptions)).hover(selector);
|
|
759
|
+
return R.ok().build({
|
|
760
|
+
driver: "camoufox",
|
|
761
|
+
message: `Hovered: ${selector}`,
|
|
762
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
if (frameOptions) await this.deps.pageController.hover(selector, frameOptions);
|
|
766
|
+
else await this.deps.pageController.hover(selector);
|
|
767
|
+
return R.ok().build({
|
|
768
|
+
message: `Hovered: ${selector}`,
|
|
769
|
+
...frameOptions ? { frame: frameOptions } : {}
|
|
770
|
+
});
|
|
771
|
+
} catch (e) {
|
|
772
|
+
return R.fail(e).build();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async handlePageScroll(args) {
|
|
776
|
+
try {
|
|
777
|
+
const x = argNumber(args, "x", 0);
|
|
778
|
+
const y = argNumber(args, "y", 0);
|
|
779
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
780
|
+
await (await this.deps.getCamoufoxPage()).evaluate((position) => {
|
|
781
|
+
window.scrollTo(position.x || 0, position.y || 0);
|
|
782
|
+
}, {
|
|
783
|
+
x,
|
|
784
|
+
y
|
|
785
|
+
});
|
|
786
|
+
return R.ok().build({
|
|
787
|
+
driver: "camoufox",
|
|
788
|
+
message: `Scrolled to: x=${x || 0}, y=${y || 0}`
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
await this.deps.pageController.scroll({
|
|
792
|
+
x,
|
|
793
|
+
y
|
|
794
|
+
});
|
|
795
|
+
return R.ok().build({ message: `Scrolled to: x=${x || 0}, y=${y || 0}` });
|
|
796
|
+
} catch (e) {
|
|
797
|
+
return R.fail(e).build();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async handlePagePressKey(args) {
|
|
801
|
+
try {
|
|
802
|
+
const key = argString(args, "key", "");
|
|
803
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
804
|
+
await (await this.deps.getCamoufoxPage()).keyboard.press(key);
|
|
805
|
+
return R.ok().build({
|
|
806
|
+
driver: "camoufox",
|
|
807
|
+
key
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
await this.deps.pageController.pressKey(key);
|
|
811
|
+
return R.ok().build({ key });
|
|
812
|
+
} catch (e) {
|
|
813
|
+
return R.fail(e).build();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/server/domains/browser/handlers/evaluation-utils.ts
|
|
819
|
+
/** Recursively remove keys listed in `fields` from any nested object/array. */
|
|
820
|
+
function filterFields(value, fields) {
|
|
821
|
+
if (Array.isArray(value)) return value.map((item) => filterFields(item, fields));
|
|
822
|
+
if (value !== null && typeof value === "object") {
|
|
823
|
+
const obj = value;
|
|
824
|
+
const out = {};
|
|
825
|
+
for (const [key, nested] of Object.entries(obj)) if (!fields.has(key)) out[key] = filterFields(nested, fields);
|
|
826
|
+
return out;
|
|
827
|
+
}
|
|
828
|
+
return value;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Recursively replace base64 payloads with a short placeholder.
|
|
832
|
+
* Catches: data:[mime];base64,<payload> and bare strings >500 chars of [A-Za-z0-9+/=]
|
|
833
|
+
*/
|
|
834
|
+
function stripBase64Values(value) {
|
|
835
|
+
if (typeof value === "string") {
|
|
836
|
+
if (/^data:[a-z+-]+\/[a-z+-]+;base64,/i.test(value)) return `[base64 ~${Math.round(value.length / 1024)}KB stripped]`;
|
|
837
|
+
if (value.length > 500 && /^[A-Za-z0-9+/=\r\n]+$/.test(value.replace(/\s/g, ""))) return `[base64 ~${value.length}chars stripped]`;
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
if (Array.isArray(value)) return value.map((item) => stripBase64Values(item));
|
|
841
|
+
if (value !== null && typeof value === "object") {
|
|
842
|
+
const obj = value;
|
|
843
|
+
const out = {};
|
|
844
|
+
for (const [key, nested] of Object.entries(obj)) out[key] = stripBase64Values(nested);
|
|
845
|
+
return out;
|
|
846
|
+
}
|
|
847
|
+
return value;
|
|
848
|
+
}
|
|
849
|
+
function applyEvaluationPostFilters(raw, detailedDataManager, options) {
|
|
850
|
+
let result = options.autoSummarize ? detailedDataManager.smartHandle(raw, options.maxSize) : raw;
|
|
851
|
+
if (options.fieldFilter && options.fieldFilter.length > 0) result = filterFields(result, new Set(options.fieldFilter));
|
|
852
|
+
if (options.stripBase64) result = stripBase64Values(result);
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/server/domains/browser/handlers/page-evaluation.ts
|
|
857
|
+
var PageEvaluationHandlers = class {
|
|
858
|
+
constructor(deps) {
|
|
859
|
+
this.deps = deps;
|
|
860
|
+
}
|
|
861
|
+
async getCamoufoxEvaluationContext(frameOptions) {
|
|
862
|
+
const page = await this.deps.getCamoufoxPage();
|
|
863
|
+
if (!frameOptions?.frameUrl && !frameOptions?.frameSelector) return page;
|
|
864
|
+
return await this.deps.pageController.resolveFrame(page, frameOptions);
|
|
865
|
+
}
|
|
866
|
+
async handlePageEvaluate(args) {
|
|
867
|
+
try {
|
|
868
|
+
const code = argString(args, "script", "") || argString(args, "code", "");
|
|
869
|
+
const autoSummarize = argBool(args, "autoSummarize", true);
|
|
870
|
+
const maxSize = argNumber(args, "maxSize", 51200);
|
|
871
|
+
const fieldFilterArg = argStringArray(args, "fieldFilter");
|
|
872
|
+
const doStripBase64 = argBool(args, "stripBase64", false);
|
|
873
|
+
const frameUrl = argString(args, "frameUrl");
|
|
874
|
+
const frameSelector = argString(args, "frameSelector");
|
|
875
|
+
const frameOptions = frameUrl || frameSelector ? {
|
|
876
|
+
frameUrl: frameUrl || void 0,
|
|
877
|
+
frameSelector: frameSelector || void 0
|
|
878
|
+
} : void 0;
|
|
879
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
880
|
+
const context = await this.getCamoufoxEvaluationContext(frameOptions);
|
|
881
|
+
const evaluateExpression = new Function(`return (${code})`);
|
|
882
|
+
const processedResult = applyEvaluationPostFilters(await context.evaluate(evaluateExpression), this.deps.detailedDataManager, {
|
|
883
|
+
autoSummarize,
|
|
884
|
+
maxSize,
|
|
885
|
+
fieldFilter: fieldFilterArg ?? void 0,
|
|
886
|
+
stripBase64: doStripBase64
|
|
887
|
+
});
|
|
888
|
+
return R.ok().build({
|
|
889
|
+
driver: "camoufox",
|
|
890
|
+
...frameOptions ? { frame: frameOptions } : {},
|
|
891
|
+
result: processedResult
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
const processedResult = applyEvaluationPostFilters(frameOptions ? await this.deps.pageController.evaluate(code, frameOptions) : await this.deps.pageController.evaluate(code), this.deps.detailedDataManager, {
|
|
895
|
+
autoSummarize,
|
|
896
|
+
maxSize,
|
|
897
|
+
fieldFilter: fieldFilterArg ?? void 0,
|
|
898
|
+
stripBase64: doStripBase64
|
|
899
|
+
});
|
|
900
|
+
return R.ok().build({
|
|
901
|
+
...frameOptions ? { frame: frameOptions } : {},
|
|
902
|
+
result: processedResult
|
|
903
|
+
});
|
|
904
|
+
} catch (e) {
|
|
905
|
+
return R.fail(e).build();
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
async handlePageScreenshot(args) {
|
|
909
|
+
try {
|
|
910
|
+
const requestedPath = argString(args, "path");
|
|
911
|
+
const type = argString(args, "type", "png");
|
|
912
|
+
const quality = argNumber(args, "quality");
|
|
913
|
+
const fullPage = argBool(args, "fullPage", false);
|
|
914
|
+
const clipArg = argObject(args, "clip");
|
|
915
|
+
const rawSelector = args.selector;
|
|
916
|
+
const selectors = [];
|
|
917
|
+
if (Array.isArray(rawSelector)) for (const s of rawSelector) {
|
|
918
|
+
const trimmed = typeof s === "string" ? s.trim() : "";
|
|
919
|
+
if (trimmed.length > 0 && trimmed.toLowerCase() !== "all") selectors.push(trimmed);
|
|
920
|
+
}
|
|
921
|
+
else if (typeof rawSelector === "string") {
|
|
922
|
+
const trimmed = rawSelector.trim();
|
|
923
|
+
if (trimmed.length > 0 && trimmed.toLowerCase() !== "all") selectors.push(trimmed);
|
|
924
|
+
}
|
|
925
|
+
if (selectors.length > 1) return this.screenshotBatch(selectors, requestedPath, type, quality);
|
|
926
|
+
const selector = selectors[0] ?? "";
|
|
927
|
+
const { absolutePath, displayPath, pathRewritten } = await resolveScreenshotOutputPath({
|
|
928
|
+
requestedPath,
|
|
929
|
+
type,
|
|
930
|
+
fallbackName: selector ? "element" : clipArg ? "region" : "page",
|
|
931
|
+
fallbackDir: "screenshots/manual"
|
|
932
|
+
});
|
|
933
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
934
|
+
const page = await this.deps.getCamoufoxPage();
|
|
935
|
+
let buffer;
|
|
936
|
+
if (selector) {
|
|
937
|
+
const element = await page.$(selector);
|
|
938
|
+
if (!element) return R.fail(`Element not found: ${selector}`).build();
|
|
939
|
+
buffer = await element.screenshot({
|
|
940
|
+
path: absolutePath,
|
|
941
|
+
type,
|
|
942
|
+
quality
|
|
943
|
+
});
|
|
944
|
+
} else buffer = await page.screenshot({
|
|
945
|
+
path: absolutePath,
|
|
946
|
+
type,
|
|
947
|
+
quality,
|
|
948
|
+
fullPage: clipArg ? false : fullPage
|
|
949
|
+
});
|
|
950
|
+
return R.ok().build({
|
|
951
|
+
driver: "camoufox",
|
|
952
|
+
selector: selector || void 0,
|
|
953
|
+
clip: clipArg || void 0,
|
|
954
|
+
message: `Screenshot taken: ${displayPath}`,
|
|
955
|
+
path: displayPath,
|
|
956
|
+
pathRewritten,
|
|
957
|
+
size: buffer?.length ?? 0
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
let buffer;
|
|
961
|
+
if (selector) {
|
|
962
|
+
const element = await (await this.deps.pageController.getPage()).$(selector);
|
|
963
|
+
if (!element) return R.fail(`Element not found: ${selector}`).build();
|
|
964
|
+
buffer = await element.screenshot({
|
|
965
|
+
path: absolutePath,
|
|
966
|
+
type,
|
|
967
|
+
quality
|
|
968
|
+
});
|
|
969
|
+
} else buffer = await this.deps.pageController.screenshot({
|
|
970
|
+
path: absolutePath,
|
|
971
|
+
type,
|
|
972
|
+
quality,
|
|
973
|
+
fullPage: clipArg ? false : fullPage,
|
|
974
|
+
clip: clipArg
|
|
975
|
+
});
|
|
976
|
+
return R.ok().build({
|
|
977
|
+
selector: selector || void 0,
|
|
978
|
+
clip: clipArg || void 0,
|
|
979
|
+
message: `Screenshot taken: ${displayPath}`,
|
|
980
|
+
path: displayPath,
|
|
981
|
+
pathRewritten,
|
|
982
|
+
size: buffer.length
|
|
983
|
+
});
|
|
984
|
+
} catch (e) {
|
|
985
|
+
return R.fail(e).build();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/** Take one screenshot per selector and return all results. */
|
|
989
|
+
async screenshotBatch(selectors, requestedPath, type, quality) {
|
|
990
|
+
const isCamoufox = this.deps.getActiveDriver() === "camoufox";
|
|
991
|
+
const results = [];
|
|
992
|
+
for (const selector of selectors) {
|
|
993
|
+
const { absolutePath, displayPath } = await resolveScreenshotOutputPath({
|
|
994
|
+
requestedPath: requestedPath ? requestedPath.replace(/(\.\w+)$/, `-${selector.replace(/[^a-zA-Z0-9]/g, "_")}$1`) : void 0,
|
|
995
|
+
type,
|
|
996
|
+
fallbackName: `element-${selector.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
997
|
+
fallbackDir: "screenshots/manual"
|
|
998
|
+
});
|
|
999
|
+
try {
|
|
1000
|
+
let size = 0;
|
|
1001
|
+
if (isCamoufox) {
|
|
1002
|
+
const element = await (await this.deps.getCamoufoxPage()).$(selector);
|
|
1003
|
+
if (!element) {
|
|
1004
|
+
results.push({
|
|
1005
|
+
selector,
|
|
1006
|
+
success: false,
|
|
1007
|
+
error: `Element not found: ${selector}`
|
|
1008
|
+
});
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
size = (await element.screenshot({
|
|
1012
|
+
path: absolutePath,
|
|
1013
|
+
type,
|
|
1014
|
+
quality
|
|
1015
|
+
}))?.length ?? 0;
|
|
1016
|
+
} else {
|
|
1017
|
+
const element = await (await this.deps.pageController.getPage()).$(selector);
|
|
1018
|
+
if (!element) {
|
|
1019
|
+
results.push({
|
|
1020
|
+
selector,
|
|
1021
|
+
success: false,
|
|
1022
|
+
error: `Element not found: ${selector}`
|
|
1023
|
+
});
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
size = (await element.screenshot({
|
|
1027
|
+
path: absolutePath,
|
|
1028
|
+
type,
|
|
1029
|
+
quality
|
|
1030
|
+
})).length;
|
|
1031
|
+
}
|
|
1032
|
+
results.push({
|
|
1033
|
+
selector,
|
|
1034
|
+
success: true,
|
|
1035
|
+
path: displayPath,
|
|
1036
|
+
size
|
|
1037
|
+
});
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
results.push({
|
|
1040
|
+
selector,
|
|
1041
|
+
success: false,
|
|
1042
|
+
error: String(err)
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return R.ok().build({
|
|
1047
|
+
mode: "batch",
|
|
1048
|
+
total: selectors.length,
|
|
1049
|
+
succeeded: results.filter((r) => r.success).length,
|
|
1050
|
+
results
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
async handlePageInjectScript(args) {
|
|
1054
|
+
try {
|
|
1055
|
+
const script = argString(args, "script", "");
|
|
1056
|
+
await this.deps.pageController.injectScript(script);
|
|
1057
|
+
return R.ok().build({ message: "Script injected" });
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
return R.fail(e).build();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async handlePageWaitForSelector(args) {
|
|
1063
|
+
try {
|
|
1064
|
+
const selector = argString(args, "selector", "");
|
|
1065
|
+
const timeout = argNumber(args, "timeout");
|
|
1066
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
1067
|
+
const page = await this.deps.getCamoufoxPage();
|
|
1068
|
+
try {
|
|
1069
|
+
await page.waitForSelector(selector, { timeout: timeout || 3e4 });
|
|
1070
|
+
const element = await page.evaluate((sel) => {
|
|
1071
|
+
const el = document.querySelector(sel);
|
|
1072
|
+
if (!el) return null;
|
|
1073
|
+
return {
|
|
1074
|
+
tagName: el.tagName.toLowerCase(),
|
|
1075
|
+
id: el.id || void 0,
|
|
1076
|
+
className: el.className || void 0,
|
|
1077
|
+
textContent: el.textContent?.trim().substring(0, 100) || void 0,
|
|
1078
|
+
attributes: Array.from(el.attributes).reduce((acc, attr) => {
|
|
1079
|
+
acc[attr.name] = attr.value;
|
|
1080
|
+
return acc;
|
|
1081
|
+
}, {})
|
|
1082
|
+
};
|
|
1083
|
+
}, selector);
|
|
1084
|
+
return R.ok().build({
|
|
1085
|
+
driver: "camoufox",
|
|
1086
|
+
element,
|
|
1087
|
+
message: `Selector appeared: ${selector}`
|
|
1088
|
+
});
|
|
1089
|
+
} catch {
|
|
1090
|
+
return R.fail(`Timeout waiting for selector: ${selector}`).build({ driver: "camoufox" });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const result = await this.deps.pageController.waitForSelector(selector, timeout);
|
|
1094
|
+
return R.ok().merge(result).build();
|
|
1095
|
+
} catch (e) {
|
|
1096
|
+
return R.fail(e).build();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region src/server/domains/browser/handlers/page-data.ts
|
|
1102
|
+
var PageDataHandlers = class {
|
|
1103
|
+
constructor(deps) {
|
|
1104
|
+
this.deps = deps;
|
|
1105
|
+
}
|
|
1106
|
+
async handleGetContent(_args) {
|
|
1107
|
+
try {
|
|
1108
|
+
const html = await this.deps.pageController.getContent();
|
|
1109
|
+
return R.ok().build({ html });
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
return R.fail(e).build();
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async handleGetTitle(_args) {
|
|
1115
|
+
try {
|
|
1116
|
+
const title = await this.deps.pageController.getTitle();
|
|
1117
|
+
return R.ok().build({ title });
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
return R.fail(e).build();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async handleGetUrl(_args) {
|
|
1123
|
+
try {
|
|
1124
|
+
const url = await this.deps.pageController.getURL();
|
|
1125
|
+
return R.ok().build({ url });
|
|
1126
|
+
} catch (e) {
|
|
1127
|
+
return R.fail(e).build();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
async handleGetText(args) {
|
|
1131
|
+
try {
|
|
1132
|
+
const selector = argString(args, "selector", "");
|
|
1133
|
+
const text = await this.deps.pageController.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent || ""`);
|
|
1134
|
+
return R.ok().build({
|
|
1135
|
+
selector,
|
|
1136
|
+
text
|
|
1137
|
+
});
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
return R.fail(e).build();
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async handleGetOuterHtml(args) {
|
|
1143
|
+
try {
|
|
1144
|
+
const selector = argString(args, "selector", "");
|
|
1145
|
+
const html = await this.deps.pageController.evaluate(`document.querySelector(${JSON.stringify(selector)})?.outerHTML || ""`);
|
|
1146
|
+
return R.ok().build({
|
|
1147
|
+
selector,
|
|
1148
|
+
html
|
|
1149
|
+
});
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
return R.fail(e).build();
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async handleGetScrollPosition(_args) {
|
|
1155
|
+
try {
|
|
1156
|
+
const pos = await this.deps.pageController.evaluate(`({
|
|
1157
|
+
scrollX: window.scrollX,
|
|
1158
|
+
scrollY: window.scrollY,
|
|
1159
|
+
maxScrollX: document.documentElement.scrollWidth - window.innerWidth,
|
|
1160
|
+
maxScrollY: document.documentElement.scrollHeight - window.innerHeight
|
|
1161
|
+
})`);
|
|
1162
|
+
return R.ok().build(pos);
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
return R.fail(e).build();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
async handlePageSetCookies(args) {
|
|
1168
|
+
try {
|
|
1169
|
+
const cookies = args.cookies;
|
|
1170
|
+
await this.deps.pageController.setCookies(cookies);
|
|
1171
|
+
return R.ok().build({ message: `Set ${cookies.length} cookies` });
|
|
1172
|
+
} catch (e) {
|
|
1173
|
+
return R.fail(e).build();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async handlePageGetCookies(_args) {
|
|
1177
|
+
try {
|
|
1178
|
+
const cookies = await this.deps.pageController.getCookies();
|
|
1179
|
+
return R.ok().build({
|
|
1180
|
+
count: cookies.length,
|
|
1181
|
+
cookies
|
|
1182
|
+
});
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
return R.fail(e).build();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async getPageCookieCount() {
|
|
1188
|
+
return (await this.deps.pageController.getCookies()).length;
|
|
1189
|
+
}
|
|
1190
|
+
async handlePageClearCookies(_args) {
|
|
1191
|
+
try {
|
|
1192
|
+
await this.deps.pageController.clearCookies();
|
|
1193
|
+
return R.ok().build({ message: "Cookies cleared" });
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
return R.fail(e).build();
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
async handlePageSetViewport(args) {
|
|
1199
|
+
try {
|
|
1200
|
+
const width = argNumber(args, "width", 0);
|
|
1201
|
+
const height = argNumber(args, "height", 0);
|
|
1202
|
+
await this.deps.pageController.setViewport(width, height);
|
|
1203
|
+
return R.ok().build({ viewport: {
|
|
1204
|
+
width,
|
|
1205
|
+
height
|
|
1206
|
+
} });
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
return R.fail(e).build();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async handlePageEmulateDevice(args) {
|
|
1212
|
+
try {
|
|
1213
|
+
const device = argString(args, "device", "");
|
|
1214
|
+
await this.deps.pageController.emulateDevice(device);
|
|
1215
|
+
return R.ok().build({ device });
|
|
1216
|
+
} catch (e) {
|
|
1217
|
+
return R.fail(e).build();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async handlePageGetLocalStorage(_args) {
|
|
1221
|
+
try {
|
|
1222
|
+
const storage = await this.deps.pageController.getLocalStorage();
|
|
1223
|
+
return R.ok().build({
|
|
1224
|
+
count: Object.keys(storage).length,
|
|
1225
|
+
storage
|
|
1226
|
+
});
|
|
1227
|
+
} catch (e) {
|
|
1228
|
+
return R.fail(e).build();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async handlePageSetLocalStorage(args) {
|
|
1232
|
+
try {
|
|
1233
|
+
const key = argString(args, "key", "");
|
|
1234
|
+
const value = argString(args, "value", "");
|
|
1235
|
+
await this.deps.pageController.setLocalStorage(key, value);
|
|
1236
|
+
return R.ok().build({ key });
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
return R.fail(e).build();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/server/domains/browser/handlers/console-handlers.ts
|
|
1244
|
+
var ConsoleHandlers = class {
|
|
1245
|
+
constructor(deps) {
|
|
1246
|
+
this.deps = deps;
|
|
1247
|
+
}
|
|
1248
|
+
async handleConsoleMonitor(args) {
|
|
1249
|
+
try {
|
|
1250
|
+
if (argString(args, "action") === "enable") {
|
|
1251
|
+
await this.deps.consoleMonitor.enable();
|
|
1252
|
+
return R.ok().build({ message: "Console monitoring enabled" });
|
|
1253
|
+
} else {
|
|
1254
|
+
await this.deps.consoleMonitor.disable();
|
|
1255
|
+
return R.ok().build({ message: "Console monitoring disabled" });
|
|
1256
|
+
}
|
|
1257
|
+
} catch (e) {
|
|
1258
|
+
return R.fail(e).build();
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async handleConsoleGetLogs(args) {
|
|
1262
|
+
try {
|
|
1263
|
+
const type = argString(args, "type");
|
|
1264
|
+
const limit = argNumber(args, "limit");
|
|
1265
|
+
const since = argNumber(args, "since");
|
|
1266
|
+
const logs = this.deps.consoleMonitor.getLogs({
|
|
1267
|
+
type,
|
|
1268
|
+
limit,
|
|
1269
|
+
since
|
|
1270
|
+
});
|
|
1271
|
+
const result = {
|
|
1272
|
+
count: logs.length,
|
|
1273
|
+
logs
|
|
1274
|
+
};
|
|
1275
|
+
const processedResult = this.deps.detailedDataManager.smartHandle(result, 51200);
|
|
1276
|
+
return R.ok().merge(processedResult).build();
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
return R.fail(e).build();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
async handleConsoleExecute(args) {
|
|
1282
|
+
try {
|
|
1283
|
+
const expression = argString(args, "expression", "");
|
|
1284
|
+
const result = await this.deps.consoleMonitor.execute(expression);
|
|
1285
|
+
return R.ok().build({ result });
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
return R.fail(e).build();
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
//#endregion
|
|
1292
|
+
//#region src/server/domains/browser/handlers/script-management.ts
|
|
1293
|
+
var ScriptManagementHandlers = class {
|
|
1294
|
+
constructor(deps) {
|
|
1295
|
+
this.deps = deps;
|
|
1296
|
+
}
|
|
1297
|
+
async handleGetAllScripts(args) {
|
|
1298
|
+
try {
|
|
1299
|
+
const includeSource = argBool(args, "includeSource", false);
|
|
1300
|
+
const MAX_SCRIPTS_CAP = SCRIPTS_MAX_CAP;
|
|
1301
|
+
const maxScripts = Math.min(argNumber(args, "maxScripts", includeSource ? 200 : 1e3), MAX_SCRIPTS_CAP);
|
|
1302
|
+
const scripts = await this.deps.scriptManager.getAllScripts(includeSource, maxScripts);
|
|
1303
|
+
const data = {
|
|
1304
|
+
count: scripts.length,
|
|
1305
|
+
scripts
|
|
1306
|
+
};
|
|
1307
|
+
const processed = this.deps.detailedDataManager.smartHandle(data);
|
|
1308
|
+
return R.ok().merge(processed).build();
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
return R.fail(e).build();
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async handleGetScriptSource(args) {
|
|
1314
|
+
try {
|
|
1315
|
+
const scriptId = argString(args, "scriptId");
|
|
1316
|
+
const url = argString(args, "url");
|
|
1317
|
+
const preview = argBool(args, "preview", true);
|
|
1318
|
+
const maxLines = argNumber(args, "maxLines", 100);
|
|
1319
|
+
const startLine = argNumber(args, "startLine");
|
|
1320
|
+
const endLine = argNumber(args, "endLine");
|
|
1321
|
+
const script = await this.deps.scriptManager.getScriptSource(scriptId, url);
|
|
1322
|
+
if (!script) return R.fail("Script not found").build();
|
|
1323
|
+
if (preview || startLine !== void 0 || endLine !== void 0) {
|
|
1324
|
+
const source = script.source || "";
|
|
1325
|
+
const lines = source.split("\n");
|
|
1326
|
+
const totalLines = lines.length;
|
|
1327
|
+
const size = source.length;
|
|
1328
|
+
let previewContent;
|
|
1329
|
+
let actualStartLine;
|
|
1330
|
+
let actualEndLine;
|
|
1331
|
+
if (startLine !== void 0 && endLine !== void 0) {
|
|
1332
|
+
actualStartLine = Math.max(1, startLine);
|
|
1333
|
+
actualEndLine = Math.min(totalLines, endLine);
|
|
1334
|
+
previewContent = lines.slice(actualStartLine - 1, actualEndLine).join("\n");
|
|
1335
|
+
} else {
|
|
1336
|
+
actualStartLine = 1;
|
|
1337
|
+
actualEndLine = Math.min(maxLines, totalLines);
|
|
1338
|
+
previewContent = lines.slice(0, maxLines).join("\n");
|
|
1339
|
+
}
|
|
1340
|
+
const result = {
|
|
1341
|
+
scriptId: script.scriptId,
|
|
1342
|
+
url: script.url,
|
|
1343
|
+
preview: true,
|
|
1344
|
+
totalLines,
|
|
1345
|
+
size,
|
|
1346
|
+
sizeKB: (size / 1024).toFixed(1) + "KB",
|
|
1347
|
+
showingLines: `${actualStartLine}-${actualEndLine}`,
|
|
1348
|
+
content: previewContent,
|
|
1349
|
+
hint: size > 51200 ? `Script is large (${(size / 1024).toFixed(1)}KB). Use startLine/endLine to get specific sections, or set preview=false to get full source (will return detailId).` : "Set preview=false to get full source"
|
|
1350
|
+
};
|
|
1351
|
+
return R.ok().build(result);
|
|
1352
|
+
}
|
|
1353
|
+
const processedScript = this.deps.detailedDataManager.smartHandle(script, 51200);
|
|
1354
|
+
return R.ok().merge(processedScript).build();
|
|
1355
|
+
} catch (e) {
|
|
1356
|
+
return R.fail(e).build();
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/server/domains/browser/handlers/captcha-handlers.ts
|
|
1362
|
+
var CaptchaHandlers = class {
|
|
1363
|
+
constructor(deps) {
|
|
1364
|
+
this.deps = deps;
|
|
1365
|
+
}
|
|
1366
|
+
async handleCaptchaDetect(_args) {
|
|
1367
|
+
try {
|
|
1368
|
+
const page = await this.deps.pageController.getPage();
|
|
1369
|
+
const result = await this.deps.captchaDetector.detect(page);
|
|
1370
|
+
return R.ok().build({
|
|
1371
|
+
captcha_detected: result.detected,
|
|
1372
|
+
captcha_info: result
|
|
1373
|
+
});
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
return R.fail(e).build();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
async handleCaptchaWait(args) {
|
|
1379
|
+
try {
|
|
1380
|
+
const timeout = argNumber(args, "timeout", this.deps.captchaTimeout);
|
|
1381
|
+
const page = await this.deps.pageController.getPage();
|
|
1382
|
+
logger.info("Waiting for CAPTCHA to be solved...");
|
|
1383
|
+
if (!await this.deps.captchaDetector.waitForCompletion(page, timeout)) return R.fail("CAPTCHA wait timed out").build();
|
|
1384
|
+
return R.ok().build({ message: "CAPTCHA solved" });
|
|
1385
|
+
} catch (e) {
|
|
1386
|
+
return R.fail(e).build();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async handleCaptchaConfig(args) {
|
|
1390
|
+
try {
|
|
1391
|
+
if (args.autoDetectCaptcha !== void 0) this.deps.setAutoDetectCaptcha(argBool(args, "autoDetectCaptcha", false));
|
|
1392
|
+
if (args.autoSwitchHeadless !== void 0) this.deps.setAutoSwitchHeadless(argBool(args, "autoSwitchHeadless", false));
|
|
1393
|
+
if (args.captchaTimeout !== void 0) this.deps.setCaptchaTimeout(argNumber(args, "captchaTimeout", 0));
|
|
1394
|
+
return R.ok().build({ config: {
|
|
1395
|
+
autoDetectCaptcha: this.deps.autoDetectCaptcha,
|
|
1396
|
+
autoSwitchHeadless: this.deps.autoSwitchHeadless,
|
|
1397
|
+
captchaTimeout: this.deps.captchaTimeout
|
|
1398
|
+
} });
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
return R.fail(e).build();
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
//#endregion
|
|
1405
|
+
//#region src/modules/stealth/CDPTimingProxy.types.ts
|
|
1406
|
+
const DEFAULT_TIMING_OPTIONS = {
|
|
1407
|
+
enabled: true,
|
|
1408
|
+
minDelayMs: 20,
|
|
1409
|
+
maxDelayMs: 80,
|
|
1410
|
+
burstMode: false
|
|
1411
|
+
};
|
|
1412
|
+
//#endregion
|
|
1413
|
+
//#region src/modules/stealth/SessionProfileManager.ts
|
|
1414
|
+
var SessionProfileManager = class SessionProfileManager {
|
|
1415
|
+
static instance = null;
|
|
1416
|
+
cachedProfile = null;
|
|
1417
|
+
static DEFAULT_TTL_SEC = 1800;
|
|
1418
|
+
static getInstance() {
|
|
1419
|
+
if (!SessionProfileManager.instance) SessionProfileManager.instance = new SessionProfileManager();
|
|
1420
|
+
return SessionProfileManager.instance;
|
|
1421
|
+
}
|
|
1422
|
+
async exportFromPage(page, options = {}) {
|
|
1423
|
+
const cookies = await page.cookies();
|
|
1424
|
+
const pageMeta = await page.evaluate(() => {
|
|
1425
|
+
const nav = navigator;
|
|
1426
|
+
const uaData = nav.userAgentData;
|
|
1427
|
+
const clientHints = {
|
|
1428
|
+
secChUa: Array.isArray(uaData?.brands) ? uaData.brands.map((b) => `"${b.brand}";v="${b.version}"`).join(", ") : void 0,
|
|
1429
|
+
secChUaMobile: typeof uaData?.mobile === "boolean" ? uaData.mobile ? "?1" : "?0" : void 0,
|
|
1430
|
+
secChUaPlatform: uaData?.platform ? `"${uaData.platform}"` : void 0
|
|
1431
|
+
};
|
|
1432
|
+
return {
|
|
1433
|
+
userAgent: nav.userAgent,
|
|
1434
|
+
platform: nav.platform,
|
|
1435
|
+
acceptLanguage: nav.language,
|
|
1436
|
+
referer: document.referrer || void 0,
|
|
1437
|
+
clientHints
|
|
1438
|
+
};
|
|
1439
|
+
});
|
|
1440
|
+
const origin = options.origin ?? this.safeOrigin(page.url());
|
|
1441
|
+
const ttlSec = options.ttlSec ?? SessionProfileManager.DEFAULT_TTL_SEC;
|
|
1442
|
+
const profile = {
|
|
1443
|
+
cookies: cookies.map((c) => ({
|
|
1444
|
+
name: c.name,
|
|
1445
|
+
value: c.value,
|
|
1446
|
+
domain: c.domain,
|
|
1447
|
+
path: c.path,
|
|
1448
|
+
expires: c.expires,
|
|
1449
|
+
size: c.size,
|
|
1450
|
+
httpOnly: c.httpOnly,
|
|
1451
|
+
secure: c.secure,
|
|
1452
|
+
session: c.session,
|
|
1453
|
+
sameSite: c.sameSite,
|
|
1454
|
+
sourceScheme: c.sourceScheme
|
|
1455
|
+
})),
|
|
1456
|
+
userAgent: pageMeta.userAgent,
|
|
1457
|
+
acceptLanguage: pageMeta.acceptLanguage,
|
|
1458
|
+
referer: options.referer ?? pageMeta.referer,
|
|
1459
|
+
clientHints: pageMeta.clientHints,
|
|
1460
|
+
platform: pageMeta.platform,
|
|
1461
|
+
origin,
|
|
1462
|
+
collectedAt: Date.now(),
|
|
1463
|
+
ttlSec
|
|
1464
|
+
};
|
|
1465
|
+
this.cachedProfile = profile;
|
|
1466
|
+
logger.info(`Session profile exported: cookies=${profile.cookies.length}, origin=${profile.origin ?? "unknown"}, ttlSec=${profile.ttlSec}`);
|
|
1467
|
+
return profile;
|
|
1468
|
+
}
|
|
1469
|
+
serialize(profile) {
|
|
1470
|
+
return JSON.stringify(profile);
|
|
1471
|
+
}
|
|
1472
|
+
deserialize(raw) {
|
|
1473
|
+
const parsed = JSON.parse(raw);
|
|
1474
|
+
return {
|
|
1475
|
+
cookies: Array.isArray(parsed.cookies) ? parsed.cookies : [],
|
|
1476
|
+
userAgent: parsed.userAgent,
|
|
1477
|
+
acceptLanguage: parsed.acceptLanguage,
|
|
1478
|
+
referer: parsed.referer,
|
|
1479
|
+
clientHints: parsed.clientHints,
|
|
1480
|
+
platform: parsed.platform,
|
|
1481
|
+
origin: parsed.origin,
|
|
1482
|
+
collectedAt: typeof parsed.collectedAt === "number" ? parsed.collectedAt : Date.now(),
|
|
1483
|
+
ttlSec: typeof parsed.ttlSec === "number" && parsed.ttlSec > 0 ? parsed.ttlSec : SessionProfileManager.DEFAULT_TTL_SEC
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
setProfile(profile) {
|
|
1487
|
+
this.cachedProfile = profile;
|
|
1488
|
+
}
|
|
1489
|
+
getProfile() {
|
|
1490
|
+
return this.cachedProfile;
|
|
1491
|
+
}
|
|
1492
|
+
getValidProfile(now = Date.now()) {
|
|
1493
|
+
if (!this.cachedProfile || this.isExpired(this.cachedProfile, now)) return null;
|
|
1494
|
+
return this.cachedProfile;
|
|
1495
|
+
}
|
|
1496
|
+
isExpired(profile, now = Date.now()) {
|
|
1497
|
+
return profile.collectedAt + profile.ttlSec * 1e3 <= now;
|
|
1498
|
+
}
|
|
1499
|
+
clearProfile() {
|
|
1500
|
+
this.cachedProfile = null;
|
|
1501
|
+
}
|
|
1502
|
+
static resetInstance() {
|
|
1503
|
+
SessionProfileManager.instance = null;
|
|
1504
|
+
}
|
|
1505
|
+
safeOrigin(url) {
|
|
1506
|
+
if (!url || url === "about:blank") return void 0;
|
|
1507
|
+
try {
|
|
1508
|
+
return new URL(url).origin;
|
|
1509
|
+
} catch {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
//#endregion
|
|
1515
|
+
//#region src/server/domains/browser/handlers/stealth-injection.ts
|
|
1516
|
+
/** Module-level jitter configuration shared across handler calls. */
|
|
1517
|
+
const jitterOptions = { ...DEFAULT_TIMING_OPTIONS };
|
|
1518
|
+
let fingerprintManagerInstance = null;
|
|
1519
|
+
const sessionProfileManager = SessionProfileManager.getInstance();
|
|
1520
|
+
async function getFingerprintManager() {
|
|
1521
|
+
if (fingerprintManagerInstance) return fingerprintManagerInstance;
|
|
1522
|
+
try {
|
|
1523
|
+
fingerprintManagerInstance = (await import("./FingerprintManager-BN4UQWnX.mjs")).FingerprintManager.getInstance();
|
|
1524
|
+
return fingerprintManagerInstance;
|
|
1525
|
+
} catch {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
var StealthInjectionHandlers = class {
|
|
1530
|
+
constructor(deps) {
|
|
1531
|
+
this.deps = deps;
|
|
1532
|
+
}
|
|
1533
|
+
async handleStealthInject(_args) {
|
|
1534
|
+
try {
|
|
1535
|
+
if (this.deps.getActiveDriver() === "camoufox") return R.ok().build({
|
|
1536
|
+
driver: "camoufox",
|
|
1537
|
+
message: "Camoufox uses C++ engine-level fingerprint spoofing — JS-layer stealth scripts are not needed and have been skipped."
|
|
1538
|
+
});
|
|
1539
|
+
const page = await this.deps.pageController.getPage();
|
|
1540
|
+
const fm = await getFingerprintManager();
|
|
1541
|
+
let fingerprintApplied = false;
|
|
1542
|
+
if (fm?.isAvailable()) try {
|
|
1543
|
+
let profile = fm.getActiveProfile();
|
|
1544
|
+
if (!profile) profile = await fm.generateFingerprint();
|
|
1545
|
+
if (profile) {
|
|
1546
|
+
await fm.injectFingerprint(page, profile);
|
|
1547
|
+
fingerprintApplied = true;
|
|
1548
|
+
}
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
logger.warn("Fingerprint injection failed, falling back to StealthScripts:", err);
|
|
1551
|
+
}
|
|
1552
|
+
await StealthScripts.injectAll(page);
|
|
1553
|
+
if (fingerprintApplied && fm) {
|
|
1554
|
+
const activeProfile = fm.getActiveProfile();
|
|
1555
|
+
const cached = sessionProfileManager.getValidProfile();
|
|
1556
|
+
const mergedProfile = {
|
|
1557
|
+
cookies: cached?.cookies ?? [],
|
|
1558
|
+
userAgent: activeProfile?.headers?.["User-Agent"] ?? cached?.userAgent,
|
|
1559
|
+
acceptLanguage: activeProfile?.headers?.["Accept-Language"] ?? cached?.acceptLanguage,
|
|
1560
|
+
referer: cached?.referer,
|
|
1561
|
+
clientHints: cached?.clientHints,
|
|
1562
|
+
platform: activeProfile?.os ?? cached?.platform,
|
|
1563
|
+
origin: cached?.origin,
|
|
1564
|
+
collectedAt: cached?.collectedAt ?? Date.now(),
|
|
1565
|
+
ttlSec: cached?.ttlSec ?? 1800
|
|
1566
|
+
};
|
|
1567
|
+
sessionProfileManager.setProfile(mergedProfile);
|
|
1568
|
+
}
|
|
1569
|
+
return R.ok().build({
|
|
1570
|
+
message: "Stealth scripts injected successfully",
|
|
1571
|
+
fingerprintApplied,
|
|
1572
|
+
_nextStepHint: "Stealth patches are now active. Next: navigate to your target URL with page_navigate. Do NOT call stealth_inject again — it only needs to run once per page."
|
|
1573
|
+
});
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
return R.fail(e).build();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
async handleStealthSetUserAgent(args) {
|
|
1579
|
+
try {
|
|
1580
|
+
const platform = argString(args, "platform", "windows");
|
|
1581
|
+
const page = await this.deps.pageController.getPage();
|
|
1582
|
+
await StealthScripts.setRealisticUserAgent(page, platform);
|
|
1583
|
+
return R.ok().build({
|
|
1584
|
+
platform,
|
|
1585
|
+
message: `User-Agent set for ${platform}`,
|
|
1586
|
+
_nextStepHint: "User-Agent is now configured. Next: call stealth_inject to apply all anti-detection patches, then page_navigate to your target URL."
|
|
1587
|
+
});
|
|
1588
|
+
} catch (e) {
|
|
1589
|
+
return R.fail(e).build();
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
async handleStealthConfigureJitter(args) {
|
|
1593
|
+
try {
|
|
1594
|
+
if (args.enabled !== void 0) jitterOptions.enabled = Boolean(args.enabled);
|
|
1595
|
+
if (typeof args.minDelayMs === "number") jitterOptions.minDelayMs = args.minDelayMs;
|
|
1596
|
+
if (typeof args.maxDelayMs === "number") jitterOptions.maxDelayMs = args.maxDelayMs;
|
|
1597
|
+
if (args.burstMode !== void 0) jitterOptions.burstMode = Boolean(args.burstMode);
|
|
1598
|
+
return R.ok().build({
|
|
1599
|
+
jitterOptions,
|
|
1600
|
+
message: `CDP timing jitter ${jitterOptions.enabled ? "enabled" : "disabled"}: ${jitterOptions.minDelayMs}-${jitterOptions.maxDelayMs}ms${jitterOptions.burstMode ? " (burst mode)" : ""}`
|
|
1601
|
+
});
|
|
1602
|
+
} catch (e) {
|
|
1603
|
+
return R.fail(e).build();
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
async handleStealthGenerateFingerprint(args) {
|
|
1607
|
+
try {
|
|
1608
|
+
if (this.deps.getActiveDriver() === "camoufox") try {
|
|
1609
|
+
const fingerprints = await import("camoufox-js/fingerprints");
|
|
1610
|
+
const os = argString(args, "os", "windows");
|
|
1611
|
+
const fp = await fingerprints.generateFingerprint(os);
|
|
1612
|
+
return R.ok().build({
|
|
1613
|
+
fingerprint: fp,
|
|
1614
|
+
driver: "camoufox",
|
|
1615
|
+
message: "Fingerprint generated using camoufox native engine. Apply via browser_launch(fingerprint=...) before launching."
|
|
1616
|
+
});
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
return R.fail(`Camoufox fingerprint generation failed: ${err instanceof Error ? err.message : String(err)}`).build();
|
|
1619
|
+
}
|
|
1620
|
+
const fm = await getFingerprintManager();
|
|
1621
|
+
if (!fm?.isAvailable()) return R.fail("fingerprint-generator/fingerprint-injector packages are not installed. Install them with: pnpm add fingerprint-generator fingerprint-injector").merge({
|
|
1622
|
+
available: false,
|
|
1623
|
+
capability: "fingerprint_generator",
|
|
1624
|
+
status: "unavailable",
|
|
1625
|
+
fix: "Install fingerprint-generator and fingerprint-injector: pnpm add fingerprint-generator fingerprint-injector"
|
|
1626
|
+
}).build();
|
|
1627
|
+
const profile = await fm.generateFingerprint({
|
|
1628
|
+
os: args.os,
|
|
1629
|
+
browser: args.browser ?? "chrome",
|
|
1630
|
+
locale: args.locale ?? "en-US"
|
|
1631
|
+
});
|
|
1632
|
+
return R.ok().build({
|
|
1633
|
+
profile,
|
|
1634
|
+
message: "Fingerprint generated and cached. It will be auto-applied on next stealth_inject."
|
|
1635
|
+
});
|
|
1636
|
+
} catch (e) {
|
|
1637
|
+
return R.fail(e).build();
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async handleStealthVerify(_args) {
|
|
1641
|
+
try {
|
|
1642
|
+
const page = await this.deps.pageController.getPage();
|
|
1643
|
+
const result = await new (await (import("./StealthVerifier-BWmPgQsv.mjs"))).StealthVerifier().verify(page);
|
|
1644
|
+
return R.ok().merge(result).build();
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
return R.fail(`Stealth verification failed: ${err instanceof Error ? err.message : String(err)}`).build();
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
async handleCamoufoxGeolocation(args) {
|
|
1650
|
+
try {
|
|
1651
|
+
const locale = argString(args, "locale");
|
|
1652
|
+
if (!locale) return R.fail("locale is required (e.g. \"en-US\", \"zh-CN\")").build();
|
|
1653
|
+
let geo;
|
|
1654
|
+
try {
|
|
1655
|
+
geo = await (await import("camoufox-js/locale")).getGeolocation(locale);
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
return R.fail(`Camoufox locale module unavailable: ${err instanceof Error ? err.message : String(err)}. Ensure camoufox-js is installed.`).merge({
|
|
1658
|
+
available: false,
|
|
1659
|
+
capability: "camoufox_locale",
|
|
1660
|
+
status: "unavailable",
|
|
1661
|
+
fix: "Install camoufox-js and fetch its browser assets: pnpm add camoufox-js && npx camoufox-js fetch"
|
|
1662
|
+
}).build();
|
|
1663
|
+
}
|
|
1664
|
+
let publicIp = null;
|
|
1665
|
+
const proxy = argString(args, "proxy");
|
|
1666
|
+
if (proxy) try {
|
|
1667
|
+
publicIp = await (await import("camoufox-js/ip")).publicIP(proxy);
|
|
1668
|
+
} catch {}
|
|
1669
|
+
return R.ok().build({
|
|
1670
|
+
locale,
|
|
1671
|
+
geolocation: geo,
|
|
1672
|
+
publicIp
|
|
1673
|
+
});
|
|
1674
|
+
} catch (e) {
|
|
1675
|
+
return R.fail(e).build();
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/server/domains/browser/handlers/framework-state.ts
|
|
1681
|
+
var FrameworkStateHandlers = class {
|
|
1682
|
+
constructor(deps) {
|
|
1683
|
+
this.deps = deps;
|
|
1684
|
+
}
|
|
1685
|
+
async handleFrameworkStateExtract(args) {
|
|
1686
|
+
const framework = argString(args, "framework", "auto");
|
|
1687
|
+
const selector = argString(args, "selector", "");
|
|
1688
|
+
const maxDepth = argNumber(args, "maxDepth", 5);
|
|
1689
|
+
try {
|
|
1690
|
+
const page = await this.deps.getActivePage();
|
|
1691
|
+
try {
|
|
1692
|
+
const cdp = await page.createCDPSession();
|
|
1693
|
+
await Promise.race([cdp.send("Runtime.evaluate", {
|
|
1694
|
+
expression: "1",
|
|
1695
|
+
returnByValue: true
|
|
1696
|
+
}), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("cdp_unreachable")), 3e3))]);
|
|
1697
|
+
} catch {
|
|
1698
|
+
throw new PrerequisiteError("CDP session unresponsive — the debugger may be blocking page evaluation. Call debugger_lifecycle({ action: 'disable' })() before framework_state_extract, or run it before debugger_lifecycle({ action: 'enable' }).");
|
|
1699
|
+
}
|
|
1700
|
+
const evalPromise = page.evaluate((opts) => {
|
|
1701
|
+
const win = window;
|
|
1702
|
+
function safeSerialize(val, depth = 0) {
|
|
1703
|
+
if (depth > 4) return "[deep]";
|
|
1704
|
+
if (val === null || val === void 0) return val;
|
|
1705
|
+
if (typeof val === "function") return "[Function]";
|
|
1706
|
+
if (typeof val !== "object") return val;
|
|
1707
|
+
if (Array.isArray(val)) return val.slice(0, 20).map((v) => safeSerialize(v, depth + 1));
|
|
1708
|
+
try {
|
|
1709
|
+
const out = {};
|
|
1710
|
+
let count = 0;
|
|
1711
|
+
for (const k of Object.keys(val)) {
|
|
1712
|
+
if (count++ > 30) {
|
|
1713
|
+
out["__truncated__"] = true;
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
out[k] = safeSerialize(val[k], depth + 1);
|
|
1717
|
+
}
|
|
1718
|
+
return out;
|
|
1719
|
+
} catch {
|
|
1720
|
+
return "[unserializable]";
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
const getRootEl = () => {
|
|
1724
|
+
if (opts.selector) return document.querySelector(opts.selector) ?? document.body;
|
|
1725
|
+
return document.getElementById("root") ?? document.getElementById("app") ?? document.querySelector("[data-reactroot]") ?? document.body;
|
|
1726
|
+
};
|
|
1727
|
+
const extractReact = () => {
|
|
1728
|
+
const rootObj = getRootEl();
|
|
1729
|
+
const fiberKey = Object.keys(rootObj).find((k) => k.startsWith("__reactFiber") || k.startsWith("__reactInternalInstance") || k.startsWith("__reactFiberContainer"));
|
|
1730
|
+
if (!fiberKey) return null;
|
|
1731
|
+
const states = [];
|
|
1732
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1733
|
+
const visitFiber = (fiber, depth) => {
|
|
1734
|
+
if (!fiber || depth > opts.maxDepth || visited.has(fiber)) return;
|
|
1735
|
+
visited.add(fiber);
|
|
1736
|
+
if (fiber["memoizedState"]) {
|
|
1737
|
+
const stateList = [];
|
|
1738
|
+
let s = fiber["memoizedState"];
|
|
1739
|
+
let guard = 0;
|
|
1740
|
+
while (s && guard++ < 20) {
|
|
1741
|
+
const queue = s["queue"];
|
|
1742
|
+
const val = s["memoizedState"] !== void 0 ? s["memoizedState"] : queue?.["lastRenderedState"];
|
|
1743
|
+
if (val !== void 0) stateList.push(safeSerialize(val));
|
|
1744
|
+
s = s["next"] ?? null;
|
|
1745
|
+
}
|
|
1746
|
+
if (stateList.length > 0) {
|
|
1747
|
+
const fiberType = fiber["type"];
|
|
1748
|
+
const componentName = typeof fiberType === "object" && fiberType !== null ? String(fiberType["name"] ?? "anonymous") : typeof fiberType === "string" ? fiberType : "anonymous";
|
|
1749
|
+
states.push({
|
|
1750
|
+
component: componentName,
|
|
1751
|
+
state: stateList
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
visitFiber(fiber["child"] ?? null, depth + 1);
|
|
1756
|
+
visitFiber(fiber["sibling"] ?? null, depth + 1);
|
|
1757
|
+
};
|
|
1758
|
+
visitFiber(rootObj[fiberKey] ?? null, 0);
|
|
1759
|
+
return states;
|
|
1760
|
+
};
|
|
1761
|
+
const extractVue3 = () => {
|
|
1762
|
+
const rootObj = getRootEl();
|
|
1763
|
+
const vueKey = Object.keys(rootObj).find((k) => k === "__vueParentComponent" || k === "__vue_app__" || k.startsWith("__vue"));
|
|
1764
|
+
if (!vueKey) return null;
|
|
1765
|
+
const comp = rootObj[vueKey];
|
|
1766
|
+
if (!comp) return null;
|
|
1767
|
+
const states = [];
|
|
1768
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1769
|
+
const visitComp = (c, depth) => {
|
|
1770
|
+
if (!c || depth > opts.maxDepth || visited.has(c)) return;
|
|
1771
|
+
visited.add(c);
|
|
1772
|
+
const setupState = safeSerialize(c["setupState"] ?? c["ctx"]);
|
|
1773
|
+
const data = safeSerialize(c["$data"] ?? c["data"]);
|
|
1774
|
+
if (setupState || data) {
|
|
1775
|
+
const compType = c["type"];
|
|
1776
|
+
states.push({
|
|
1777
|
+
component: compType?.["__name"] ?? "unknown",
|
|
1778
|
+
setupState,
|
|
1779
|
+
data
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
const children = c["subTree"]?.["children"];
|
|
1783
|
+
if (Array.isArray(children)) {
|
|
1784
|
+
for (const child of children) if (child?.["component"]) visitComp(child["component"], depth + 1);
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
visitComp(comp, 0);
|
|
1788
|
+
return states;
|
|
1789
|
+
};
|
|
1790
|
+
const extractVue2 = () => {
|
|
1791
|
+
const rootObj = getRootEl();
|
|
1792
|
+
const vueKey = Object.keys(rootObj).find((k) => k === "__vue__");
|
|
1793
|
+
if (!vueKey) return null;
|
|
1794
|
+
const vm = rootObj[vueKey];
|
|
1795
|
+
if (!vm) return null;
|
|
1796
|
+
const states = [];
|
|
1797
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1798
|
+
const visitVm = (v, depth) => {
|
|
1799
|
+
if (!v || depth > opts.maxDepth || visited.has(v)) return;
|
|
1800
|
+
visited.add(v);
|
|
1801
|
+
const options = v["$options"];
|
|
1802
|
+
states.push({
|
|
1803
|
+
component: options?.["name"] ?? "unknown",
|
|
1804
|
+
data: safeSerialize(v["$data"])
|
|
1805
|
+
});
|
|
1806
|
+
const children = v["$children"];
|
|
1807
|
+
if (Array.isArray(children)) for (const child of children) visitVm(child, depth + 1);
|
|
1808
|
+
};
|
|
1809
|
+
visitVm(vm, 0);
|
|
1810
|
+
return states;
|
|
1811
|
+
};
|
|
1812
|
+
const extractSvelte = () => {
|
|
1813
|
+
const states = [];
|
|
1814
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1815
|
+
const svelteEls = document.querySelectorAll("[class]");
|
|
1816
|
+
const candidates = [getRootEl(), ...Array.from(svelteEls)];
|
|
1817
|
+
let foundAny = false;
|
|
1818
|
+
for (const el of candidates) {
|
|
1819
|
+
const obj = el;
|
|
1820
|
+
if (!Object.keys(obj).some((k) => k === "$$" || k === "__svelte_meta" || k.startsWith("__s"))) continue;
|
|
1821
|
+
foundAny = true;
|
|
1822
|
+
const ctx = obj["$$"];
|
|
1823
|
+
if (!ctx || visited.has(ctx)) continue;
|
|
1824
|
+
visited.add(ctx);
|
|
1825
|
+
const componentName = (obj["__svelte_meta"]?.["loc"])?.["file"];
|
|
1826
|
+
const ctxArray = ctx["ctx"];
|
|
1827
|
+
const stateObj = {};
|
|
1828
|
+
if (Array.isArray(ctxArray)) {
|
|
1829
|
+
let idx = 0;
|
|
1830
|
+
for (const val of ctxArray.slice(0, 20)) {
|
|
1831
|
+
if (val !== void 0 && typeof val !== "function") stateObj[`$${idx}`] = safeSerialize(val);
|
|
1832
|
+
idx++;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const fragment = ctx["fragment"];
|
|
1836
|
+
if (Object.keys(stateObj).length > 0 || fragment) states.push({
|
|
1837
|
+
component: componentName ?? el.tagName?.toLowerCase() ?? "svelte-component",
|
|
1838
|
+
state: [stateObj],
|
|
1839
|
+
...componentName ? { file: componentName } : {}
|
|
1840
|
+
});
|
|
1841
|
+
if (states.length >= 50) break;
|
|
1842
|
+
}
|
|
1843
|
+
return foundAny ? states : null;
|
|
1844
|
+
};
|
|
1845
|
+
const extractSolid = () => {
|
|
1846
|
+
const states = [];
|
|
1847
|
+
const dx = win["_$DX"];
|
|
1848
|
+
const hy = win["_$HY"];
|
|
1849
|
+
if (!dx && !hy) {
|
|
1850
|
+
if (!document.querySelector("[data-hk]")) return null;
|
|
1851
|
+
states.push({
|
|
1852
|
+
component: "SolidRoot",
|
|
1853
|
+
state: [{ _note: "Solid detected via hydration markers; install solid-devtools for full state extraction" }]
|
|
1854
|
+
});
|
|
1855
|
+
return states;
|
|
1856
|
+
}
|
|
1857
|
+
if (dx) {
|
|
1858
|
+
const roots = dx["roots"];
|
|
1859
|
+
if (roots && typeof roots === "object") {
|
|
1860
|
+
const entries = roots instanceof Map ? Array.from(roots.values()) : Object.values(roots);
|
|
1861
|
+
let count = 0;
|
|
1862
|
+
for (const root of entries) {
|
|
1863
|
+
if (count++ >= opts.maxDepth * 10) break;
|
|
1864
|
+
const name = root["name"] ?? "SolidComponent";
|
|
1865
|
+
const value = root["value"] ?? root["state"];
|
|
1866
|
+
states.push({
|
|
1867
|
+
component: name,
|
|
1868
|
+
state: value ? [safeSerialize(value)] : []
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (hy && states.length === 0) states.push({
|
|
1874
|
+
component: "SolidHydration",
|
|
1875
|
+
state: [safeSerialize(hy)]
|
|
1876
|
+
});
|
|
1877
|
+
return states.length > 0 ? states : null;
|
|
1878
|
+
};
|
|
1879
|
+
const extractPreact = () => {
|
|
1880
|
+
const rootObj = getRootEl();
|
|
1881
|
+
const rootKeys = Object.keys(rootObj);
|
|
1882
|
+
if (rootKeys.some((k) => k.startsWith("__reactFiber") || k.startsWith("__reactInternalInstance"))) return null;
|
|
1883
|
+
if (!rootKeys.some((k) => k === "__k" || k === "__e" || k === "_dom")) return null;
|
|
1884
|
+
const states = [];
|
|
1885
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1886
|
+
const visitVNode = (vnode, depth) => {
|
|
1887
|
+
if (!vnode || depth > opts.maxDepth || visited.has(vnode)) return;
|
|
1888
|
+
visited.add(vnode);
|
|
1889
|
+
const component = vnode["__c"];
|
|
1890
|
+
if (component) {
|
|
1891
|
+
const compState = component["state"];
|
|
1892
|
+
const compProps = component["props"];
|
|
1893
|
+
const hooks = component["__H"];
|
|
1894
|
+
const hookStates = [];
|
|
1895
|
+
if (hooks) {
|
|
1896
|
+
const list = hooks["__"];
|
|
1897
|
+
if (Array.isArray(list)) for (const h of list.slice(0, 20)) {
|
|
1898
|
+
const val = h["__"] ?? h["_value"];
|
|
1899
|
+
if (val !== void 0) hookStates.push(safeSerialize(val));
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const typeName = vnode["type"];
|
|
1903
|
+
const name = typeof typeName === "function" ? typeName["displayName"] ?? typeName["name"] ?? "PreactComponent" : typeof typeName === "string" ? typeName : "PreactComponent";
|
|
1904
|
+
if (compState || hookStates.length > 0) states.push({
|
|
1905
|
+
component: String(name),
|
|
1906
|
+
state: hookStates.length > 0 ? hookStates : compState ? [safeSerialize(compState)] : [],
|
|
1907
|
+
...compProps ? { props: safeSerialize(compProps) } : {}
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
const children = vnode["__k"];
|
|
1911
|
+
if (Array.isArray(children)) {
|
|
1912
|
+
for (const child of children) if (child) visitVNode(child, depth + 1);
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
const rootVNode = rootObj["__k"];
|
|
1916
|
+
if (Array.isArray(rootVNode)) {
|
|
1917
|
+
for (const vn of rootVNode) if (vn) visitVNode(vn, 0);
|
|
1918
|
+
} else if (rootObj["_children"]) {
|
|
1919
|
+
const alt = rootObj["_children"];
|
|
1920
|
+
if (Array.isArray(alt)) {
|
|
1921
|
+
for (const vn of alt) if (vn) visitVNode(vn, 0);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return states.length > 0 ? states : null;
|
|
1925
|
+
};
|
|
1926
|
+
const extractMetaFramework = () => {
|
|
1927
|
+
const nextData = win["__NEXT_DATA__"];
|
|
1928
|
+
if (nextData) return {
|
|
1929
|
+
framework: "nextjs",
|
|
1930
|
+
route: nextData["page"],
|
|
1931
|
+
buildId: nextData["buildId"],
|
|
1932
|
+
runtimeConfig: safeSerialize(nextData["runtimeConfig"]),
|
|
1933
|
+
props: safeSerialize(nextData["props"])
|
|
1934
|
+
};
|
|
1935
|
+
const nuxt = win["__NUXT__"];
|
|
1936
|
+
if (nuxt) {
|
|
1937
|
+
if (nuxt["config"] !== void 0 || nuxt["_errors"] !== void 0) return {
|
|
1938
|
+
framework: "nuxt3",
|
|
1939
|
+
state: safeSerialize(nuxt["state"]),
|
|
1940
|
+
config: safeSerialize(nuxt["config"]),
|
|
1941
|
+
payload: safeSerialize(nuxt["data"])
|
|
1942
|
+
};
|
|
1943
|
+
return {
|
|
1944
|
+
framework: "nuxt2",
|
|
1945
|
+
state: safeSerialize(nuxt["state"]),
|
|
1946
|
+
serverRendered: nuxt["serverRendered"]
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
return null;
|
|
1950
|
+
};
|
|
1951
|
+
const rootObj = getRootEl();
|
|
1952
|
+
const keys = Object.keys(rootObj);
|
|
1953
|
+
const hasReactMarker = keys.some((k) => k.startsWith("__reactFiber") || k.startsWith("__reactInternalInstance") || k.startsWith("__reactFiberContainer"));
|
|
1954
|
+
const hasVue3Marker = keys.some((k) => k === "__vueParentComponent" || k === "__vue_app__");
|
|
1955
|
+
const hasVue2Marker = keys.some((k) => k === "__vue__");
|
|
1956
|
+
const hasSvelteMarker = keys.some((k) => k === "$$" || k === "__svelte_meta" || k.startsWith("__s"));
|
|
1957
|
+
const hasSolidMarker = win["_$DX"] !== void 0 || win["_$HY"] !== void 0 || Boolean(document.querySelector("[data-hk]"));
|
|
1958
|
+
const hasPreactMarker = keys.some((k) => k === "__k" || k === "__e" || k === "_dom" || k === "_children");
|
|
1959
|
+
let detectedFramework = opts.framework;
|
|
1960
|
+
if (detectedFramework === "preact" && hasReactMarker) detectedFramework = "react";
|
|
1961
|
+
if (detectedFramework === "auto") {
|
|
1962
|
+
if (hasReactMarker) detectedFramework = "react";
|
|
1963
|
+
else if (hasVue3Marker) detectedFramework = "vue3";
|
|
1964
|
+
else if (hasVue2Marker) detectedFramework = "vue2";
|
|
1965
|
+
else if (hasSvelteMarker) detectedFramework = "svelte";
|
|
1966
|
+
else if (hasSolidMarker) detectedFramework = "solid";
|
|
1967
|
+
else if (hasPreactMarker) detectedFramework = "preact";
|
|
1968
|
+
}
|
|
1969
|
+
let states = null;
|
|
1970
|
+
if (detectedFramework === "react" || detectedFramework === "auto") states = extractReact();
|
|
1971
|
+
if (!states && (detectedFramework === "vue3" || detectedFramework === "auto")) states = extractVue3();
|
|
1972
|
+
if (!states && (detectedFramework === "vue2" || detectedFramework === "auto")) states = extractVue2();
|
|
1973
|
+
if (!states && (detectedFramework === "svelte" || detectedFramework === "auto")) states = extractSvelte();
|
|
1974
|
+
if (!states && (detectedFramework === "solid" || detectedFramework === "auto")) states = extractSolid();
|
|
1975
|
+
if (!states && (detectedFramework === "preact" || detectedFramework === "auto")) states = extractPreact();
|
|
1976
|
+
const meta = extractMetaFramework();
|
|
1977
|
+
return {
|
|
1978
|
+
detected: detectedFramework,
|
|
1979
|
+
states: states ?? [],
|
|
1980
|
+
found: states !== null && states.length > 0,
|
|
1981
|
+
...meta ? { meta } : {}
|
|
1982
|
+
};
|
|
1983
|
+
}, {
|
|
1984
|
+
framework,
|
|
1985
|
+
selector,
|
|
1986
|
+
maxDepth
|
|
1987
|
+
});
|
|
1988
|
+
const result = await Promise.race([evalPromise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("page.evaluate timed out after 30000ms")), 3e4))]);
|
|
1989
|
+
return R.ok().build(result);
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
return R.fail(error).build();
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
//#endregion
|
|
1996
|
+
//#region src/server/domains/browser/handlers/indexeddb-dump.ts
|
|
1997
|
+
var IndexedDBDumpHandlers = class {
|
|
1998
|
+
constructor(deps) {
|
|
1999
|
+
this.deps = deps;
|
|
2000
|
+
}
|
|
2001
|
+
async handleIndexedDBDump(args) {
|
|
2002
|
+
const database = argString(args, "database", "");
|
|
2003
|
+
const store = argString(args, "store", "");
|
|
2004
|
+
const maxRecords = argNumber(args, "maxRecords", 100);
|
|
2005
|
+
try {
|
|
2006
|
+
const result = await (await this.deps.getActivePage()).evaluate(async (opts) => {
|
|
2007
|
+
const dbList = await indexedDB.databases();
|
|
2008
|
+
const output = {};
|
|
2009
|
+
for (const dbInfo of dbList) {
|
|
2010
|
+
if (!dbInfo.name) continue;
|
|
2011
|
+
if (opts.database && dbInfo.name !== opts.database) continue;
|
|
2012
|
+
const dbName = dbInfo.name;
|
|
2013
|
+
let db;
|
|
2014
|
+
try {
|
|
2015
|
+
db = await new Promise((resolve, reject) => {
|
|
2016
|
+
const req = dbInfo.version ? indexedDB.open(dbName, dbInfo.version) : indexedDB.open(dbName);
|
|
2017
|
+
req.addEventListener("success", () => resolve(req.result), { once: true });
|
|
2018
|
+
req.addEventListener("error", () => reject(req.error), { once: true });
|
|
2019
|
+
});
|
|
2020
|
+
} catch {
|
|
2021
|
+
output[dbName] = { __error__: ["failed to open"] };
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
const storeNames = Array.from(db.objectStoreNames);
|
|
2025
|
+
const dbData = {};
|
|
2026
|
+
for (const storeName of storeNames) {
|
|
2027
|
+
if (opts.store && storeName !== opts.store) continue;
|
|
2028
|
+
try {
|
|
2029
|
+
dbData[storeName] = await new Promise((resolve, reject) => {
|
|
2030
|
+
try {
|
|
2031
|
+
const req = db.transaction(storeName, "readonly").objectStore(storeName).getAll();
|
|
2032
|
+
req.addEventListener("success", () => resolve(req.result.slice(0, opts.maxRecords)), { once: true });
|
|
2033
|
+
req.addEventListener("error", () => reject(req.error), { once: true });
|
|
2034
|
+
} catch (e) {
|
|
2035
|
+
reject(e);
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
} catch {
|
|
2039
|
+
dbData[storeName] = ["__error reading store__"];
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
db.close();
|
|
2043
|
+
output[dbName] = dbData;
|
|
2044
|
+
}
|
|
2045
|
+
return output;
|
|
2046
|
+
}, {
|
|
2047
|
+
database,
|
|
2048
|
+
store,
|
|
2049
|
+
maxRecords
|
|
2050
|
+
});
|
|
2051
|
+
return R.ok().build(result);
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
return R.fail(error).build();
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
//#endregion
|
|
2058
|
+
//#region src/server/domains/browser/handlers/detailed-data.ts
|
|
2059
|
+
var DetailedDataHandlers = class {
|
|
2060
|
+
constructor(deps) {
|
|
2061
|
+
this.deps = deps;
|
|
2062
|
+
}
|
|
2063
|
+
async handleGetDetailedData(args) {
|
|
2064
|
+
try {
|
|
2065
|
+
const detailId = argString(args, "detailId", "");
|
|
2066
|
+
const path = argString(args, "path");
|
|
2067
|
+
const data = this.deps.detailedDataManager.retrieve(detailId, path);
|
|
2068
|
+
return R.ok().build({
|
|
2069
|
+
detailId,
|
|
2070
|
+
path: path || "full",
|
|
2071
|
+
data
|
|
2072
|
+
});
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
return R.fail(error).set("hint", "DetailId may have expired (TTL: 10 minutes) or is invalid").build();
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
//#endregion
|
|
2079
|
+
//#region src/server/domains/browser/handlers/target-evaluation.ts
|
|
2080
|
+
var TargetEvaluationHandlers = class {
|
|
2081
|
+
constructor(deps) {
|
|
2082
|
+
this.deps = deps;
|
|
2083
|
+
}
|
|
2084
|
+
async handleBrowserEvaluateCdpTarget(args) {
|
|
2085
|
+
try {
|
|
2086
|
+
const code = argString(args, "script", "") || argString(args, "code", "");
|
|
2087
|
+
const autoSummarize = argBool(args, "autoSummarize", true);
|
|
2088
|
+
const maxSize = argNumber(args, "maxSize", 51200);
|
|
2089
|
+
const fieldFilterArg = argStringArray(args, "fieldFilter");
|
|
2090
|
+
const doStripBase64 = argBool(args, "stripBase64", false);
|
|
2091
|
+
const returnByValue = argBool(args, "returnByValue", true);
|
|
2092
|
+
const awaitPromise = argBool(args, "awaitPromise", true);
|
|
2093
|
+
if (!code) return R.fail("code is required").build();
|
|
2094
|
+
const activeTarget = this.deps.pageController.getAttachedTargetInfo();
|
|
2095
|
+
if (!activeTarget) return R.fail("No CDP target is currently attached. Call browser_attach_cdp_target(targetId=\"...\") first.").build();
|
|
2096
|
+
const processedResult = applyEvaluationPostFilters(await this.deps.pageController.evaluateAttachedTarget(code, {
|
|
2097
|
+
returnByValue,
|
|
2098
|
+
awaitPromise
|
|
2099
|
+
}), this.deps.detailedDataManager, {
|
|
2100
|
+
autoSummarize,
|
|
2101
|
+
maxSize,
|
|
2102
|
+
fieldFilter: fieldFilterArg ?? void 0,
|
|
2103
|
+
stripBase64: doStripBase64
|
|
2104
|
+
});
|
|
2105
|
+
return R.ok().build({
|
|
2106
|
+
target: activeTarget,
|
|
2107
|
+
result: processedResult
|
|
2108
|
+
});
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
logger.error("Failed to evaluate in CDP target:", error);
|
|
2111
|
+
return R.fail(error instanceof Error ? error.message : String(error)).build();
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
//#endregion
|
|
2116
|
+
//#region src/server/domains/browser/handlers/target-control.ts
|
|
2117
|
+
var TargetControlHandlers = class {
|
|
2118
|
+
constructor(deps) {
|
|
2119
|
+
this.deps = deps;
|
|
2120
|
+
}
|
|
2121
|
+
markMonitoringContextChanged(context) {
|
|
2122
|
+
try {
|
|
2123
|
+
this.deps.consoleMonitor.markContextChanged();
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
logger.warn(`[${context}] Failed to mark monitoring context as stale: ${error instanceof Error ? error.message : String(error)}`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
safeOrigin(url) {
|
|
2129
|
+
if (!url) return null;
|
|
2130
|
+
try {
|
|
2131
|
+
return new URL(url).origin;
|
|
2132
|
+
} catch {
|
|
2133
|
+
return null;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async clearAttachedTargetContext(context) {
|
|
2137
|
+
const activeTarget = this.deps.collector.getAttachedTargetInfo();
|
|
2138
|
+
if (!activeTarget) return {
|
|
2139
|
+
detached: false,
|
|
2140
|
+
targetId: null,
|
|
2141
|
+
type: null
|
|
2142
|
+
};
|
|
2143
|
+
const detached = await this.deps.collector.detachCdpTarget();
|
|
2144
|
+
if (detached) logger.info(`[${context}] Detached active CDP target ${activeTarget.targetId} before switching page context`);
|
|
2145
|
+
return {
|
|
2146
|
+
detached,
|
|
2147
|
+
targetId: activeTarget.targetId,
|
|
2148
|
+
type: activeTarget.type ?? null
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
async handleBrowserListCdpTargets(args) {
|
|
2152
|
+
try {
|
|
2153
|
+
const type = argString(args, "type");
|
|
2154
|
+
const types = argStringArray(args, "types");
|
|
2155
|
+
const targetId = argString(args, "targetId");
|
|
2156
|
+
const urlPattern = argString(args, "urlPattern");
|
|
2157
|
+
const titlePattern = argString(args, "titlePattern");
|
|
2158
|
+
const attachedOnly = argBool(args, "attachedOnly", false);
|
|
2159
|
+
const discoverOOPIF = argBool(args, "discoverOOPIF", true);
|
|
2160
|
+
const targets = await this.deps.collector.listCdpTargets({
|
|
2161
|
+
type: type ?? void 0,
|
|
2162
|
+
types: types ?? void 0,
|
|
2163
|
+
targetId: targetId ?? void 0,
|
|
2164
|
+
urlPattern: urlPattern ?? void 0,
|
|
2165
|
+
titlePattern: titlePattern ?? void 0,
|
|
2166
|
+
attachedOnly,
|
|
2167
|
+
discoverOOPIF
|
|
2168
|
+
});
|
|
2169
|
+
const activeTarget = this.deps.collector.getAttachedTargetInfo();
|
|
2170
|
+
const contextMeta = this.deps.getTabRegistry().getContextMeta();
|
|
2171
|
+
const pages = await this.deps.collector.listPages();
|
|
2172
|
+
const currentTab = typeof contextMeta.tabIndex === "number" ? pages[contextMeta.tabIndex] : void 0;
|
|
2173
|
+
const currentTabUrl = currentTab?.url ?? null;
|
|
2174
|
+
const currentTabOrigin = this.safeOrigin(currentTabUrl);
|
|
2175
|
+
const enrichedTargets = targets.map((target) => {
|
|
2176
|
+
const targetUrl = target.url;
|
|
2177
|
+
const targetOrigin = this.safeOrigin(targetUrl);
|
|
2178
|
+
const currentTabMatch = currentTabUrl !== null && targetUrl === currentTabUrl;
|
|
2179
|
+
const sameOriginAsCurrentTab = currentTabOrigin !== null && targetOrigin !== null && currentTabOrigin === targetOrigin;
|
|
2180
|
+
const isActiveTarget = activeTarget?.targetId === target.targetId;
|
|
2181
|
+
const relationHints = [];
|
|
2182
|
+
if (isActiveTarget) relationHints.push("active_target");
|
|
2183
|
+
if (currentTabMatch) relationHints.push("matches_current_tab_url");
|
|
2184
|
+
if (!currentTabMatch && sameOriginAsCurrentTab) relationHints.push("same_origin_as_current_tab");
|
|
2185
|
+
if (target.openerId && activeTarget?.targetId === target.openerId) relationHints.push("opened_by_active_target");
|
|
2186
|
+
if (target.openerId && !relationHints.includes("opened_by_active_target")) relationHints.push("has_opener_target");
|
|
2187
|
+
if (target.openerFrameId) relationHints.push("has_opener_frame");
|
|
2188
|
+
return {
|
|
2189
|
+
...target,
|
|
2190
|
+
isActiveTarget,
|
|
2191
|
+
matchesCurrentTabUrl: currentTabMatch,
|
|
2192
|
+
sameOriginAsCurrentTab,
|
|
2193
|
+
relationHints
|
|
2194
|
+
};
|
|
2195
|
+
});
|
|
2196
|
+
return R.ok().build({
|
|
2197
|
+
count: enrichedTargets.length,
|
|
2198
|
+
activeTarget,
|
|
2199
|
+
currentTab: currentTab ? {
|
|
2200
|
+
index: currentTab.index,
|
|
2201
|
+
url: currentTab.url,
|
|
2202
|
+
title: currentTab.title
|
|
2203
|
+
} : null,
|
|
2204
|
+
filters: {
|
|
2205
|
+
type: type ?? null,
|
|
2206
|
+
types: types ?? null,
|
|
2207
|
+
targetId: targetId ?? null,
|
|
2208
|
+
urlPattern: urlPattern ?? null,
|
|
2209
|
+
titlePattern: titlePattern ?? null,
|
|
2210
|
+
attachedOnly
|
|
2211
|
+
},
|
|
2212
|
+
targets: enrichedTargets
|
|
2213
|
+
});
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
logger.error("Failed to list CDP targets:", error);
|
|
2216
|
+
return R.fail(error instanceof Error ? error.message : String(error)).build();
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
async handleBrowserAttachCdpTarget(args) {
|
|
2220
|
+
try {
|
|
2221
|
+
const targetId = argString(args, "targetId");
|
|
2222
|
+
if (!targetId) throw new Error("targetId is required");
|
|
2223
|
+
const target = await this.deps.collector.attachCdpTarget(targetId);
|
|
2224
|
+
this.markMonitoringContextChanged("browser_attach_cdp_target");
|
|
2225
|
+
return R.ok().build({
|
|
2226
|
+
attached: true,
|
|
2227
|
+
target
|
|
2228
|
+
});
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
logger.error("Failed to attach CDP target:", error);
|
|
2231
|
+
return R.fail(error instanceof Error ? error.message : String(error)).build();
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
async handleBrowserDetachCdpTarget(_args) {
|
|
2235
|
+
try {
|
|
2236
|
+
const activeTarget = this.deps.collector.getAttachedTargetInfo();
|
|
2237
|
+
const detached = await this.deps.collector.detachCdpTarget();
|
|
2238
|
+
if (detached) this.markMonitoringContextChanged("browser_detach_cdp_target");
|
|
2239
|
+
return R.ok().build({
|
|
2240
|
+
detached,
|
|
2241
|
+
targetId: activeTarget?.targetId ?? null
|
|
2242
|
+
});
|
|
2243
|
+
} catch (error) {
|
|
2244
|
+
logger.error("Failed to detach CDP target:", error);
|
|
2245
|
+
return R.fail(error instanceof Error ? error.message : String(error)).build();
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
//#endregion
|
|
2250
|
+
//#region src/server/domains/browser/handlers/js-heap.ts
|
|
2251
|
+
const NODE_TYPE_NAMES = [
|
|
2252
|
+
"hidden",
|
|
2253
|
+
"array",
|
|
2254
|
+
"string",
|
|
2255
|
+
"object",
|
|
2256
|
+
"code",
|
|
2257
|
+
"closure",
|
|
2258
|
+
"regexp",
|
|
2259
|
+
"number",
|
|
2260
|
+
"native",
|
|
2261
|
+
"synthetic",
|
|
2262
|
+
"concatenated string",
|
|
2263
|
+
"sliced string",
|
|
2264
|
+
"symbol",
|
|
2265
|
+
"bigint"
|
|
2266
|
+
];
|
|
2267
|
+
function isRecord$1(value) {
|
|
2268
|
+
return typeof value === "object" && value !== null;
|
|
2269
|
+
}
|
|
2270
|
+
function isCDPPageLike(value) {
|
|
2271
|
+
return isRecord$1(value) && typeof value.createCDPSession === "function";
|
|
2272
|
+
}
|
|
2273
|
+
function isHeapSnapshotChunk(value) {
|
|
2274
|
+
return isRecord$1(value) && typeof value.chunk === "string";
|
|
2275
|
+
}
|
|
2276
|
+
var JSHeapSearchHandlers = class {
|
|
2277
|
+
detailedDataManager;
|
|
2278
|
+
constructor(deps) {
|
|
2279
|
+
this.deps = deps;
|
|
2280
|
+
this.detailedDataManager = DetailedDataManager.getInstance();
|
|
2281
|
+
}
|
|
2282
|
+
async handleJSHeapSearch(args) {
|
|
2283
|
+
const pattern = argString(args, "pattern", "") || argString(args, "query", "");
|
|
2284
|
+
const maxResults = argNumber(args, "maxResults", 50);
|
|
2285
|
+
const caseSensitive = argBool(args, "caseSensitive", false);
|
|
2286
|
+
if (!pattern) return R.fail("pattern is required").build();
|
|
2287
|
+
return cdpLimit(async () => {
|
|
2288
|
+
let cdpSession = null;
|
|
2289
|
+
let ownedSession = false;
|
|
2290
|
+
try {
|
|
2291
|
+
const page = await this.deps.getActivePage();
|
|
2292
|
+
if (!isCDPPageLike(page)) throw new Error("Active page does not support CDP session creation");
|
|
2293
|
+
cdpSession = await page.createCDPSession();
|
|
2294
|
+
ownedSession = true;
|
|
2295
|
+
logger.info("[js_heap_search] Taking heap snapshot", {
|
|
2296
|
+
patternLength: pattern.length,
|
|
2297
|
+
caseSensitive,
|
|
2298
|
+
maxResults
|
|
2299
|
+
});
|
|
2300
|
+
await cdpSession.send("HeapProfiler.enable");
|
|
2301
|
+
const snapshotChunks = [];
|
|
2302
|
+
let snapshotSize = 0;
|
|
2303
|
+
cdpSession.on("HeapProfiler.addHeapSnapshotChunk", (params) => {
|
|
2304
|
+
if (isHeapSnapshotChunk(params)) {
|
|
2305
|
+
snapshotChunks.push(params.chunk);
|
|
2306
|
+
snapshotSize += params.chunk.length;
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
await cdpSession.send("HeapProfiler.takeHeapSnapshot", {
|
|
2310
|
+
reportProgress: false,
|
|
2311
|
+
treatGlobalObjectsAsRoots: true,
|
|
2312
|
+
captureNumericValue: false
|
|
2313
|
+
});
|
|
2314
|
+
await cdpSession.send("HeapProfiler.disable");
|
|
2315
|
+
logger.info(`[js_heap_search] Snapshot size: ${(snapshotSize / 1024).toFixed(1)} KB`);
|
|
2316
|
+
const snapshotData = snapshotChunks.join("");
|
|
2317
|
+
snapshotChunks.length = 0;
|
|
2318
|
+
const matches = this.searchSnapshot(snapshotData, pattern, maxResults, caseSensitive);
|
|
2319
|
+
const result = {
|
|
2320
|
+
success: true,
|
|
2321
|
+
pattern,
|
|
2322
|
+
caseSensitive,
|
|
2323
|
+
snapshotSizeKB: Math.round(snapshotSize / 1024),
|
|
2324
|
+
matchCount: matches.length,
|
|
2325
|
+
truncated: matches.length >= maxResults,
|
|
2326
|
+
matches,
|
|
2327
|
+
tip: matches.length > 0 ? "Use page_evaluate to inspect the objects at the paths found. E.g., eval the objectPath as a JS expression." : "No matches found. The value may be encrypted, compressed, or stored in a non-string form."
|
|
2328
|
+
};
|
|
2329
|
+
return R.ok().build(this.detailedDataManager.smartHandle(result, 51200));
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
logger.error("[js_heap_search] Error:", error);
|
|
2332
|
+
return R.fail(error).build();
|
|
2333
|
+
} finally {
|
|
2334
|
+
if (ownedSession && cdpSession) try {
|
|
2335
|
+
await cdpSession.detach();
|
|
2336
|
+
} catch {}
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
searchSnapshot(snapshotData, pattern, maxResults, caseSensitive) {
|
|
2341
|
+
try {
|
|
2342
|
+
let parsed;
|
|
2343
|
+
try {
|
|
2344
|
+
parsed = JSON.parse(snapshotData);
|
|
2345
|
+
} catch {
|
|
2346
|
+
return [];
|
|
2347
|
+
}
|
|
2348
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return [];
|
|
2349
|
+
const snapshot = parsed;
|
|
2350
|
+
const stringsRaw = snapshot.strings;
|
|
2351
|
+
const nodesRaw = snapshot.nodes;
|
|
2352
|
+
const snapshotMeta = typeof snapshot.snapshot === "object" && snapshot.snapshot !== null ? snapshot.snapshot : null;
|
|
2353
|
+
const meta = snapshotMeta && typeof snapshotMeta.meta === "object" && snapshotMeta.meta !== null ? snapshotMeta.meta : null;
|
|
2354
|
+
const nodeFieldsRaw = meta?.node_fields;
|
|
2355
|
+
const nodeTypesRaw = meta?.node_types;
|
|
2356
|
+
if (!Array.isArray(nodeFieldsRaw) || !Array.isArray(stringsRaw) || !Array.isArray(nodesRaw)) return [];
|
|
2357
|
+
const nodeFieldCount = nodeFieldsRaw.length;
|
|
2358
|
+
if (nodeFieldCount === 0) return [];
|
|
2359
|
+
const typeIdx = nodeFieldsRaw.indexOf("type");
|
|
2360
|
+
const nameIdx = nodeFieldsRaw.indexOf("name");
|
|
2361
|
+
const idIdx = nodeFieldsRaw.indexOf("id");
|
|
2362
|
+
if (typeIdx < 0 || nameIdx < 0) return [];
|
|
2363
|
+
const nodeTypeTable = Array.isArray(nodeTypesRaw) && Array.isArray(nodeTypesRaw[0]) ? nodeTypesRaw[0] : [];
|
|
2364
|
+
const searchStr = caseSensitive ? pattern : pattern.toLowerCase();
|
|
2365
|
+
const matches = [];
|
|
2366
|
+
const nodeCount = Math.floor(nodesRaw.length / nodeFieldCount);
|
|
2367
|
+
const stringsArr = stringsRaw;
|
|
2368
|
+
for (let i = 0; i < nodeCount && matches.length < maxResults; i++) {
|
|
2369
|
+
const base = i * nodeFieldCount;
|
|
2370
|
+
const typeOrdinal = nodesRaw[base + typeIdx];
|
|
2371
|
+
const nameOrdinal = nodesRaw[base + nameIdx];
|
|
2372
|
+
if (typeof nameOrdinal !== "number" || nameOrdinal < 0 || nameOrdinal >= stringsArr.length) continue;
|
|
2373
|
+
const tableName = nodeTypeTable[typeOrdinal];
|
|
2374
|
+
const nodeTypeName = (typeof tableName === "string" ? tableName : void 0) ?? NODE_TYPE_NAMES[typeOrdinal] ?? `type_${typeOrdinal}`;
|
|
2375
|
+
if (nodeTypeName !== "string" && nodeTypeName !== "concatenated string" && nodeTypeName !== "sliced string") continue;
|
|
2376
|
+
const value = stringsArr[nameOrdinal];
|
|
2377
|
+
if (typeof value !== "string") continue;
|
|
2378
|
+
if (!(caseSensitive ? value : value.toLowerCase()).includes(searchStr)) continue;
|
|
2379
|
+
const rawId = idIdx >= 0 ? nodesRaw[base + idIdx] : void 0;
|
|
2380
|
+
const nodeId = rawId !== void 0 ? rawId : i;
|
|
2381
|
+
matches.push({
|
|
2382
|
+
nodeId,
|
|
2383
|
+
nodeType: nodeTypeName,
|
|
2384
|
+
value: value.length > 200 ? `${value.slice(0, 200)}…` : value,
|
|
2385
|
+
objectPath: `[HeapNode #${nodeId}]`,
|
|
2386
|
+
nameHint: value.slice(0, 80)
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
return matches;
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
logger.warn("[js_heap_search] Snapshot parse error:", error);
|
|
2392
|
+
return [];
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
//#endregion
|
|
2397
|
+
//#region src/server/domains/browser/handlers/tab-workflow.ts
|
|
2398
|
+
const TAB_ACTIONS = new Set([
|
|
2399
|
+
"alias_bind",
|
|
2400
|
+
"alias_open",
|
|
2401
|
+
"navigate",
|
|
2402
|
+
"wait_for",
|
|
2403
|
+
"context_set",
|
|
2404
|
+
"context_get",
|
|
2405
|
+
"transfer",
|
|
2406
|
+
"list",
|
|
2407
|
+
"clear"
|
|
2408
|
+
]);
|
|
2409
|
+
function isRecord(value) {
|
|
2410
|
+
return typeof value === "object" && value !== null;
|
|
2411
|
+
}
|
|
2412
|
+
function isTabAction(value) {
|
|
2413
|
+
return typeof value === "string" && TAB_ACTIONS.has(value);
|
|
2414
|
+
}
|
|
2415
|
+
function isTabPageLike(value) {
|
|
2416
|
+
return isRecord(value) && typeof value.goto === "function" && typeof value.waitForSelector === "function" && typeof value.evaluate === "function" && typeof value.url === "function" && typeof value.title === "function";
|
|
2417
|
+
}
|
|
2418
|
+
function isCamoufoxPageLike(value) {
|
|
2419
|
+
if (!isTabPageLike(value) || !isRecord(value)) return false;
|
|
2420
|
+
return typeof value.context === "function";
|
|
2421
|
+
}
|
|
2422
|
+
function isBrowserLike(value) {
|
|
2423
|
+
return isRecord(value) && typeof value.newPage === "function" && typeof value.pages === "function";
|
|
2424
|
+
}
|
|
2425
|
+
function readRequiredString(value) {
|
|
2426
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2427
|
+
}
|
|
2428
|
+
function readAliasIndex(value) {
|
|
2429
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2430
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
2431
|
+
const parsed = Number(value);
|
|
2432
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2433
|
+
}
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2436
|
+
function readTimeout(value, fallback) {
|
|
2437
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
2438
|
+
return fallback;
|
|
2439
|
+
}
|
|
2440
|
+
var TabWorkflowHandlers = class {
|
|
2441
|
+
constructor(deps) {
|
|
2442
|
+
this.deps = deps;
|
|
2443
|
+
}
|
|
2444
|
+
get registry() {
|
|
2445
|
+
return this.deps.getTabRegistry();
|
|
2446
|
+
}
|
|
2447
|
+
async handleTabWorkflow(args) {
|
|
2448
|
+
const action = args.action;
|
|
2449
|
+
try {
|
|
2450
|
+
if (!isTabAction(action)) return R.fail(`Unknown action: "${String(action)}". Valid: list, alias_bind, alias_open, navigate, wait_for, context_set, context_get, transfer, clear`).build();
|
|
2451
|
+
switch (action) {
|
|
2452
|
+
case "list": return this.listAliases();
|
|
2453
|
+
case "clear": return this.clearState();
|
|
2454
|
+
case "alias_bind": return await this.aliasBind(args);
|
|
2455
|
+
case "alias_open": return await this.aliasOpen(args);
|
|
2456
|
+
case "navigate": return await this.navigateAlias(args);
|
|
2457
|
+
case "wait_for": return await this.waitFor(args);
|
|
2458
|
+
case "context_set": return this.contextSet(args);
|
|
2459
|
+
case "context_get": return this.contextGet(args);
|
|
2460
|
+
case "transfer": return await this.transfer(args);
|
|
2461
|
+
}
|
|
2462
|
+
} catch (err) {
|
|
2463
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2464
|
+
logger.error("[tab_workflow] Action failed", {
|
|
2465
|
+
action: typeof action === "string" ? action : String(action),
|
|
2466
|
+
alias: typeof args.alias === "string" ? args.alias : void 0,
|
|
2467
|
+
fromAlias: typeof args.fromAlias === "string" ? args.fromAlias : void 0,
|
|
2468
|
+
error: errorMsg
|
|
2469
|
+
});
|
|
2470
|
+
return R.fail(errorMsg).build();
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
listAliases() {
|
|
2474
|
+
const info = this.registry.getCurrentTabInfo(this.deps.getActiveDriver());
|
|
2475
|
+
const context = this.registry.getSharedContextMap();
|
|
2476
|
+
return R.ok().build({
|
|
2477
|
+
aliases: info.aliases,
|
|
2478
|
+
staleAliases: info.staleAliases,
|
|
2479
|
+
currentPageId: info.currentPageId,
|
|
2480
|
+
currentIndex: info.currentIndex,
|
|
2481
|
+
currentUrl: info.url,
|
|
2482
|
+
context
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
clearState() {
|
|
2486
|
+
this.registry.clear();
|
|
2487
|
+
return R.ok().build({ cleared: true });
|
|
2488
|
+
}
|
|
2489
|
+
async aliasBind(args) {
|
|
2490
|
+
const alias = readRequiredString(args.alias);
|
|
2491
|
+
const index = readAliasIndex(args.index);
|
|
2492
|
+
if (!alias) return R.fail("alias is required").build();
|
|
2493
|
+
if (index === null) return R.fail("index is required").build();
|
|
2494
|
+
await this.reconcilePages();
|
|
2495
|
+
const pageId = this.registry.bindAliasByIndex(alias, index);
|
|
2496
|
+
if (!pageId) return R.fail(`No active page at index ${index}. Use browser_list_tabs to check available pages.`).build();
|
|
2497
|
+
return R.ok().build({ bound: {
|
|
2498
|
+
alias,
|
|
2499
|
+
index,
|
|
2500
|
+
pageId
|
|
2501
|
+
} });
|
|
2502
|
+
}
|
|
2503
|
+
async aliasOpen(args) {
|
|
2504
|
+
const alias = readRequiredString(args.alias);
|
|
2505
|
+
const url = readRequiredString(args.url);
|
|
2506
|
+
if (!alias) return R.fail("alias is required").build();
|
|
2507
|
+
if (!url) return R.fail("url is required").build();
|
|
2508
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
2509
|
+
const currentPage = await this.deps.getCamoufoxPage();
|
|
2510
|
+
if (!isCamoufoxPageLike(currentPage)) return R.fail("Cannot open new tab: camoufox page context not accessible").build();
|
|
2511
|
+
const context = currentPage.context();
|
|
2512
|
+
const newPage = await context.newPage();
|
|
2513
|
+
await newPage.goto(url, { waitUntil: "domcontentloaded" });
|
|
2514
|
+
const idx = context.pages().indexOf(newPage);
|
|
2515
|
+
const pageTitle = await newPage.title();
|
|
2516
|
+
const pageId = this.registry.registerPage(newPage, {
|
|
2517
|
+
index: idx,
|
|
2518
|
+
url: newPage.url(),
|
|
2519
|
+
title: pageTitle
|
|
2520
|
+
});
|
|
2521
|
+
this.registry.bindAlias(alias, pageId);
|
|
2522
|
+
return R.ok().build({
|
|
2523
|
+
alias,
|
|
2524
|
+
index: idx,
|
|
2525
|
+
pageId,
|
|
2526
|
+
url: newPage.url(),
|
|
2527
|
+
title: pageTitle
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
const browser = await this.getBrowserFromController();
|
|
2531
|
+
if (!browser) return R.fail("Cannot open new tab: browser instance not accessible via PageController").build();
|
|
2532
|
+
const newPage = await browser.newPage();
|
|
2533
|
+
await newPage.goto(url, { waitUntil: "domcontentloaded" });
|
|
2534
|
+
const idx = (await browser.pages()).indexOf(newPage);
|
|
2535
|
+
const pageTitle = await newPage.title();
|
|
2536
|
+
const pageId = this.registry.registerPage(newPage, {
|
|
2537
|
+
index: idx,
|
|
2538
|
+
url: newPage.url(),
|
|
2539
|
+
title: pageTitle
|
|
2540
|
+
});
|
|
2541
|
+
this.registry.bindAlias(alias, pageId);
|
|
2542
|
+
return R.ok().build({
|
|
2543
|
+
alias,
|
|
2544
|
+
index: idx,
|
|
2545
|
+
pageId,
|
|
2546
|
+
url: newPage.url(),
|
|
2547
|
+
title: pageTitle
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
async navigateAlias(args) {
|
|
2551
|
+
const alias = readRequiredString(args.alias);
|
|
2552
|
+
const url = readRequiredString(args.url);
|
|
2553
|
+
if (!alias) return R.fail("alias is required").build();
|
|
2554
|
+
if (!url) return R.fail("url is required").build();
|
|
2555
|
+
const page = await this.getPageByAlias(alias);
|
|
2556
|
+
if (!page) return R.fail(`No tab found for alias "${alias}". Use alias_bind or alias_open first.`).build();
|
|
2557
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
2558
|
+
return R.ok().build({
|
|
2559
|
+
alias,
|
|
2560
|
+
navigated: url,
|
|
2561
|
+
currentUrl: page.url()
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
async waitFor(args) {
|
|
2565
|
+
const alias = readRequiredString(args.alias);
|
|
2566
|
+
const selector = readRequiredString(args.selector);
|
|
2567
|
+
const text = readRequiredString(args.waitForText);
|
|
2568
|
+
const timeoutMs = readTimeout(args.timeoutMs, 1e4);
|
|
2569
|
+
if (!alias) return R.fail("alias is required").build();
|
|
2570
|
+
if (!selector && !text) return R.fail("selector or waitForText is required").build();
|
|
2571
|
+
const page = await this.getPageByAlias(alias);
|
|
2572
|
+
if (!page) return R.fail(`No tab found for alias "${alias}"`).build();
|
|
2573
|
+
if (selector) {
|
|
2574
|
+
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
2575
|
+
return R.ok().build({
|
|
2576
|
+
alias,
|
|
2577
|
+
waitedFor: selector,
|
|
2578
|
+
found: true
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
const waitText = text;
|
|
2582
|
+
const start = Date.now();
|
|
2583
|
+
while (Date.now() - start < timeoutMs) {
|
|
2584
|
+
const bodyTextValue = await page.evaluate(() => document.body.innerText);
|
|
2585
|
+
if ((typeof bodyTextValue === "string" ? bodyTextValue : String(bodyTextValue ?? "")).includes(waitText)) return R.ok().build({
|
|
2586
|
+
alias,
|
|
2587
|
+
waitedForText: waitText,
|
|
2588
|
+
found: true
|
|
2589
|
+
});
|
|
2590
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2591
|
+
}
|
|
2592
|
+
return R.fail(`Timeout waiting for text "${waitText}" in tab "${alias}"`).build();
|
|
2593
|
+
}
|
|
2594
|
+
contextSet(args) {
|
|
2595
|
+
const key = readRequiredString(args.key);
|
|
2596
|
+
const value = args.value;
|
|
2597
|
+
if (!key) return R.fail("key is required").build();
|
|
2598
|
+
this.registry.setSharedContext(key, value);
|
|
2599
|
+
return R.ok().build({ set: {
|
|
2600
|
+
key,
|
|
2601
|
+
value
|
|
2602
|
+
} });
|
|
2603
|
+
}
|
|
2604
|
+
contextGet(args) {
|
|
2605
|
+
const key = readRequiredString(args.key);
|
|
2606
|
+
if (!key) return R.fail("key is required").build();
|
|
2607
|
+
const { value, found } = this.registry.getSharedContext(key);
|
|
2608
|
+
return R.ok().build({
|
|
2609
|
+
key,
|
|
2610
|
+
value,
|
|
2611
|
+
found
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
async transfer(args) {
|
|
2615
|
+
const fromAlias = readRequiredString(args.fromAlias);
|
|
2616
|
+
const key = readRequiredString(args.key);
|
|
2617
|
+
const expression = readRequiredString(args.expression);
|
|
2618
|
+
if (!fromAlias) return R.fail("fromAlias is required").build();
|
|
2619
|
+
if (!key) return R.fail("key is required").build();
|
|
2620
|
+
if (!expression) return R.fail("expression is required").build();
|
|
2621
|
+
const page = await this.getPageByAlias(fromAlias);
|
|
2622
|
+
if (!page) return R.fail(`No tab found for alias "${fromAlias}"`).build();
|
|
2623
|
+
const value = await page.evaluate(expression);
|
|
2624
|
+
this.registry.setSharedContext(key, value);
|
|
2625
|
+
return R.ok().build({ transferred: {
|
|
2626
|
+
fromAlias,
|
|
2627
|
+
key,
|
|
2628
|
+
value
|
|
2629
|
+
} });
|
|
2630
|
+
}
|
|
2631
|
+
async getPageByAlias(alias) {
|
|
2632
|
+
const pageId = this.registry.resolveAlias(alias);
|
|
2633
|
+
if (!pageId) return null;
|
|
2634
|
+
const page = this.registry.getPageById(pageId);
|
|
2635
|
+
if (page && isTabPageLike(page)) return page;
|
|
2636
|
+
await this.reconcilePages();
|
|
2637
|
+
const retryPage = this.registry.getPageById(pageId);
|
|
2638
|
+
if (retryPage && isTabPageLike(retryPage)) return retryPage;
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2641
|
+
async reconcilePages() {
|
|
2642
|
+
if (this.deps.getActiveDriver() === "camoufox") {
|
|
2643
|
+
const page = await this.deps.getCamoufoxPage();
|
|
2644
|
+
if (isCamoufoxPageLike(page)) {
|
|
2645
|
+
const pages = page.context().pages();
|
|
2646
|
+
const meta = await Promise.all(pages.map(async (p, i) => ({
|
|
2647
|
+
index: i,
|
|
2648
|
+
url: p.url(),
|
|
2649
|
+
title: await p.title()
|
|
2650
|
+
})));
|
|
2651
|
+
this.registry.reconcilePages(pages, meta);
|
|
2652
|
+
}
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
const browser = await this.getBrowserFromController();
|
|
2656
|
+
if (!browser) return;
|
|
2657
|
+
const pages = await browser.pages();
|
|
2658
|
+
const meta = await Promise.all(pages.map(async (p, i) => ({
|
|
2659
|
+
index: i,
|
|
2660
|
+
url: p.url(),
|
|
2661
|
+
title: await p.title()
|
|
2662
|
+
})));
|
|
2663
|
+
this.registry.reconcilePages(pages, meta);
|
|
2664
|
+
}
|
|
2665
|
+
async getBrowserFromController() {
|
|
2666
|
+
const pc = this.deps.getPageController();
|
|
2667
|
+
if (!isRecord(pc) || typeof pc.getBrowser !== "function") return null;
|
|
2668
|
+
const browser = await pc.getBrowser();
|
|
2669
|
+
return isBrowserLike(browser) ? browser : null;
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
//#endregion
|
|
2673
|
+
//#region src/server/domains/browser/handlers/jsdom-tools.ts
|
|
2674
|
+
/**
|
|
2675
|
+
* jsdom-tools.ts — Headless DOM analysis tools backed by the `jsdom` package.
|
|
2676
|
+
*
|
|
2677
|
+
* Provides 5 tools for offline HTML parsing, DOM querying, sandboxed script
|
|
2678
|
+
* execution, serialization and cookie management. Each call creates or
|
|
2679
|
+
* references a session keyed by UUID. Sessions auto-expire after
|
|
2680
|
+
* {@link SESSION_TTL_MS} of inactivity.
|
|
2681
|
+
*/
|
|
2682
|
+
const RUN_SCRIPTS_MODES = new Set([
|
|
2683
|
+
"none",
|
|
2684
|
+
"outside-only",
|
|
2685
|
+
"dangerously"
|
|
2686
|
+
]);
|
|
2687
|
+
const COOKIE_ACTIONS = new Set([
|
|
2688
|
+
"get",
|
|
2689
|
+
"set",
|
|
2690
|
+
"clear"
|
|
2691
|
+
]);
|
|
2692
|
+
/** Maximum HTML input size to prevent unbounded memory allocation. */
|
|
2693
|
+
const MAX_HTML_SIZE_BYTES = 10 * 1024 * 1024;
|
|
2694
|
+
/** Session lifetime in milliseconds. Configurable via env in future. */
|
|
2695
|
+
const SESSION_TTL_MS = 600 * 1e3;
|
|
2696
|
+
var JsdomHandlers = class {
|
|
2697
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2698
|
+
createSessionId() {
|
|
2699
|
+
return randomUUID();
|
|
2700
|
+
}
|
|
2701
|
+
scheduleExpiry(sessionId) {
|
|
2702
|
+
const timer = setTimeout(() => {
|
|
2703
|
+
logger.debug(`JSDOM session ${sessionId} expired after ${SESSION_TTL_MS}ms`);
|
|
2704
|
+
this.closeSession(sessionId);
|
|
2705
|
+
}, SESSION_TTL_MS);
|
|
2706
|
+
timer.unref?.();
|
|
2707
|
+
return timer;
|
|
2708
|
+
}
|
|
2709
|
+
refreshSessionExpiry(sessionId, session) {
|
|
2710
|
+
clearTimeout(session.timer);
|
|
2711
|
+
session.timer = this.scheduleExpiry(sessionId);
|
|
2712
|
+
}
|
|
2713
|
+
getSession(sessionId) {
|
|
2714
|
+
const session = this.sessions.get(sessionId);
|
|
2715
|
+
if (!session) throw new Error(`JSDOM session not found or expired: ${sessionId}`);
|
|
2716
|
+
this.refreshSessionExpiry(sessionId, session);
|
|
2717
|
+
return session;
|
|
2718
|
+
}
|
|
2719
|
+
closeSession(sessionId) {
|
|
2720
|
+
const session = this.sessions.get(sessionId);
|
|
2721
|
+
if (!session) return;
|
|
2722
|
+
clearTimeout(session.timer);
|
|
2723
|
+
try {
|
|
2724
|
+
session.dom.window.close();
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
logger.debug(`JSDOM window close error: ${String(err)}`);
|
|
2727
|
+
}
|
|
2728
|
+
this.sessions.delete(sessionId);
|
|
2729
|
+
}
|
|
2730
|
+
/** Close all active sessions. Called on server shutdown. */
|
|
2731
|
+
closeAll() {
|
|
2732
|
+
for (const id of Array.from(this.sessions.keys())) this.closeSession(id);
|
|
2733
|
+
}
|
|
2734
|
+
async handleJsdomParse(args) {
|
|
2735
|
+
try {
|
|
2736
|
+
const html = argStringRequired(args, "html");
|
|
2737
|
+
if (Buffer.byteLength(html, "utf8") > MAX_HTML_SIZE_BYTES) return R.fail(`HTML input exceeds ${MAX_HTML_SIZE_BYTES / 1024 / 1024}MB limit. Provide smaller HTML or use a URL.`).build();
|
|
2738
|
+
const url = argString(args, "url", "about:blank");
|
|
2739
|
+
const contentType = argString(args, "contentType", "text/html");
|
|
2740
|
+
const runScripts = argEnum(args, "runScripts", RUN_SCRIPTS_MODES, "none");
|
|
2741
|
+
const includeNodeLocations = argBool(args, "includeNodeLocations", false);
|
|
2742
|
+
const pretendToBeVisual = argBool(args, "pretendToBeVisual", false);
|
|
2743
|
+
const referrer = argString(args, "referrer", "");
|
|
2744
|
+
const options = {
|
|
2745
|
+
url,
|
|
2746
|
+
contentType,
|
|
2747
|
+
includeNodeLocations,
|
|
2748
|
+
pretendToBeVisual,
|
|
2749
|
+
storageQuota: argNumber(args, "storageQuotaBytes", 1e6)
|
|
2750
|
+
};
|
|
2751
|
+
if (runScripts !== "none") options.runScripts = runScripts;
|
|
2752
|
+
if (referrer) options.referrer = referrer;
|
|
2753
|
+
const { JSDOM } = await import("jsdom");
|
|
2754
|
+
const dom = new JSDOM(html, options);
|
|
2755
|
+
const sessionId = this.createSessionId();
|
|
2756
|
+
const session = {
|
|
2757
|
+
dom,
|
|
2758
|
+
url,
|
|
2759
|
+
runScripts,
|
|
2760
|
+
includeNodeLocations,
|
|
2761
|
+
createdAt: Date.now(),
|
|
2762
|
+
timer: this.scheduleExpiry(sessionId)
|
|
2763
|
+
};
|
|
2764
|
+
this.sessions.set(sessionId, session);
|
|
2765
|
+
const doc = dom.window.document;
|
|
2766
|
+
return R.ok().set("sessionId", sessionId).set("title", doc.title || "").set("url", url).set("contentType", contentType).set("runScripts", runScripts).set("ttlMs", SESSION_TTL_MS).set("activeSessions", this.sessions.size).set("stats", {
|
|
2767
|
+
elements: doc.getElementsByTagName("*").length,
|
|
2768
|
+
scripts: doc.getElementsByTagName("script").length,
|
|
2769
|
+
links: doc.getElementsByTagName("a").length,
|
|
2770
|
+
images: doc.getElementsByTagName("img").length,
|
|
2771
|
+
stylesheets: doc.querySelectorAll("link[rel=\"stylesheet\"], style").length
|
|
2772
|
+
}).build();
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
return R.fail(error).build();
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
async handleJsdomQuery(args) {
|
|
2778
|
+
try {
|
|
2779
|
+
const sessionId = argStringRequired(args, "sessionId");
|
|
2780
|
+
const selector = argStringRequired(args, "selector");
|
|
2781
|
+
const maxResults = argNumber(args, "maxResults", 50);
|
|
2782
|
+
const includeHtml = argBool(args, "includeHtml", false);
|
|
2783
|
+
const includeText = argBool(args, "includeText", true);
|
|
2784
|
+
const includeLocation = argBool(args, "includeLocation", false);
|
|
2785
|
+
const attributes = argStringArray(args, "attributes");
|
|
2786
|
+
const session = this.getSession(sessionId);
|
|
2787
|
+
const doc = session.dom.window.document;
|
|
2788
|
+
const all = Array.from(doc.querySelectorAll(selector));
|
|
2789
|
+
const results = all.slice(0, maxResults).map((el) => {
|
|
2790
|
+
const item = { tag: el.tagName.toLowerCase() };
|
|
2791
|
+
if (attributes.length > 0) {
|
|
2792
|
+
const picked = {};
|
|
2793
|
+
for (const name of attributes) picked[name] = el.getAttribute(name);
|
|
2794
|
+
item.attributes = picked;
|
|
2795
|
+
} else {
|
|
2796
|
+
const full = {};
|
|
2797
|
+
for (const attr of Array.from(el.attributes)) full[attr.name] = attr.value;
|
|
2798
|
+
item.attributes = full;
|
|
2799
|
+
}
|
|
2800
|
+
if (includeText) item.text = (el.textContent ?? "").trim();
|
|
2801
|
+
if (includeHtml) item.html = el.outerHTML;
|
|
2802
|
+
if (includeLocation && session.includeNodeLocations) try {
|
|
2803
|
+
item.location = session.dom.nodeLocation(el) ?? null;
|
|
2804
|
+
} catch {
|
|
2805
|
+
item.location = null;
|
|
2806
|
+
}
|
|
2807
|
+
return item;
|
|
2808
|
+
});
|
|
2809
|
+
return R.ok().set("sessionId", sessionId).set("selector", selector).set("matched", all.length).set("returned", results.length).set("results", results).build();
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
return R.fail(error).build();
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
async handleJsdomExecute(args) {
|
|
2815
|
+
try {
|
|
2816
|
+
const sessionId = argStringRequired(args, "sessionId");
|
|
2817
|
+
const code = argStringRequired(args, "code");
|
|
2818
|
+
const timeoutHintMs = argNumber(args, "timeoutMs", 5e3);
|
|
2819
|
+
const session = this.getSession(sessionId);
|
|
2820
|
+
if (session.runScripts === "none") return R.fail("JSDOM session was created with runScripts=\"none\". Re-parse with runScripts=\"outside-only\" or \"dangerously\" to execute code.").build();
|
|
2821
|
+
const logs = [];
|
|
2822
|
+
const window = session.dom.window;
|
|
2823
|
+
const originalConsole = window.console;
|
|
2824
|
+
window.console = createCapturingConsole(originalConsole, logs);
|
|
2825
|
+
let result;
|
|
2826
|
+
let errorMessage = null;
|
|
2827
|
+
try {
|
|
2828
|
+
result = window.eval(code);
|
|
2829
|
+
} catch (err) {
|
|
2830
|
+
errorMessage = err instanceof Error ? err.message : String(err);
|
|
2831
|
+
} finally {
|
|
2832
|
+
window.console = originalConsole;
|
|
2833
|
+
}
|
|
2834
|
+
if (errorMessage !== null) return R.fail(errorMessage).set("consoleLogs", logs).build();
|
|
2835
|
+
return R.ok().set("sessionId", sessionId).set("result", safeSerialize(result)).set("consoleLogs", logs).set("timeoutHintMs", timeoutHintMs).build();
|
|
2836
|
+
} catch (error) {
|
|
2837
|
+
return R.fail(error).build();
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
async handleJsdomSerialize(args) {
|
|
2841
|
+
try {
|
|
2842
|
+
const sessionId = argStringRequired(args, "sessionId");
|
|
2843
|
+
const pretty = argBool(args, "pretty", false);
|
|
2844
|
+
const fragment = argString(args, "selector", "");
|
|
2845
|
+
const session = this.getSession(sessionId);
|
|
2846
|
+
let html;
|
|
2847
|
+
if (fragment) {
|
|
2848
|
+
const element = session.dom.window.document.querySelector(fragment);
|
|
2849
|
+
if (!element) return R.fail(`No element matches selector: ${fragment}`).build();
|
|
2850
|
+
html = element.outerHTML;
|
|
2851
|
+
} else html = session.dom.serialize();
|
|
2852
|
+
if (pretty) html = prettyPrintHtml(html);
|
|
2853
|
+
return R.ok().set("sessionId", sessionId).set("bytes", Buffer.byteLength(html, "utf8")).set("pretty", pretty).set("html", html).build();
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
return R.fail(error).build();
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
async handleJsdomCookies(args) {
|
|
2859
|
+
try {
|
|
2860
|
+
const sessionId = argStringRequired(args, "sessionId");
|
|
2861
|
+
const action = argEnum(args, "action", COOKIE_ACTIONS, "get");
|
|
2862
|
+
const session = this.getSession(sessionId);
|
|
2863
|
+
const jar = session.dom.cookieJar;
|
|
2864
|
+
const cookieUrl = argString(args, "url", session.url);
|
|
2865
|
+
if (action === "get") {
|
|
2866
|
+
const cookies = await jar.getCookies(cookieUrl);
|
|
2867
|
+
return R.ok().set("sessionId", sessionId).set("url", cookieUrl).set("cookies", cookies.map(serializeCookie)).build();
|
|
2868
|
+
}
|
|
2869
|
+
if (action === "set") {
|
|
2870
|
+
const cookie = argObject(args, "cookie");
|
|
2871
|
+
if (!cookie) return R.fail("cookie object required for action=\"set\"").build();
|
|
2872
|
+
const cookieStr = typeof cookie.raw === "string" ? cookie.raw : buildCookieString(cookie);
|
|
2873
|
+
await jar.setCookie(cookieStr, cookieUrl);
|
|
2874
|
+
return R.ok().set("sessionId", sessionId).set("action", "set").set("cookie", cookieStr).build();
|
|
2875
|
+
}
|
|
2876
|
+
const store = jar.store;
|
|
2877
|
+
if (store && typeof store.removeAllCookies === "function") await new Promise((resolve, reject) => store.removeAllCookies((err) => err ? reject(err) : resolve()));
|
|
2878
|
+
return R.ok().set("sessionId", sessionId).set("action", "clear").build();
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
return R.fail(error).build();
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
};
|
|
2884
|
+
function serializeCookie(c) {
|
|
2885
|
+
return {
|
|
2886
|
+
key: c.key ?? "",
|
|
2887
|
+
value: c.value ?? "",
|
|
2888
|
+
domain: c.domain ?? null,
|
|
2889
|
+
path: c.path ?? null,
|
|
2890
|
+
expires: c.expires instanceof Date ? c.expires.toISOString() : c.expires ?? null,
|
|
2891
|
+
httpOnly: c.httpOnly === true,
|
|
2892
|
+
secure: c.secure === true,
|
|
2893
|
+
sameSite: c.sameSite ?? null
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function buildCookieString(cookie) {
|
|
2897
|
+
const name = String(cookie.name ?? cookie.key ?? "");
|
|
2898
|
+
const value = String(cookie.value ?? "");
|
|
2899
|
+
if (!name) throw new Error("cookie.name (or cookie.key) is required");
|
|
2900
|
+
const parts = [`${name}=${value}`];
|
|
2901
|
+
if (typeof cookie.domain === "string") parts.push(`Domain=${cookie.domain}`);
|
|
2902
|
+
if (typeof cookie.path === "string") parts.push(`Path=${cookie.path}`);
|
|
2903
|
+
if (typeof cookie.expires === "string") parts.push(`Expires=${cookie.expires}`);
|
|
2904
|
+
if (typeof cookie.maxAge === "number") parts.push(`Max-Age=${cookie.maxAge}`);
|
|
2905
|
+
if (cookie.secure === true) parts.push("Secure");
|
|
2906
|
+
if (cookie.httpOnly === true) parts.push("HttpOnly");
|
|
2907
|
+
if (typeof cookie.sameSite === "string") parts.push(`SameSite=${cookie.sameSite}`);
|
|
2908
|
+
return parts.join("; ");
|
|
2909
|
+
}
|
|
2910
|
+
function createCapturingConsole(original, logs) {
|
|
2911
|
+
const levels = [
|
|
2912
|
+
"log",
|
|
2913
|
+
"info",
|
|
2914
|
+
"warn",
|
|
2915
|
+
"error",
|
|
2916
|
+
"debug",
|
|
2917
|
+
"trace"
|
|
2918
|
+
];
|
|
2919
|
+
const proxy = {};
|
|
2920
|
+
for (const level of levels) proxy[level] = (...callArgs) => {
|
|
2921
|
+
logs.push({
|
|
2922
|
+
level,
|
|
2923
|
+
args: callArgs.map((x) => safeSerialize(x))
|
|
2924
|
+
});
|
|
2925
|
+
const orig = original?.[level];
|
|
2926
|
+
if (typeof orig === "function") try {
|
|
2927
|
+
orig.apply(original, callArgs);
|
|
2928
|
+
} catch {}
|
|
2929
|
+
};
|
|
2930
|
+
return proxy;
|
|
2931
|
+
}
|
|
2932
|
+
function safeSerialize(value) {
|
|
2933
|
+
if (value === null || value === void 0) return value;
|
|
2934
|
+
const t = typeof value;
|
|
2935
|
+
if (t === "string" || t === "number" || t === "boolean") return value;
|
|
2936
|
+
if (t === "bigint") return `${value.toString()}n`;
|
|
2937
|
+
if (t === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
2938
|
+
if (t === "symbol") return String(value);
|
|
2939
|
+
try {
|
|
2940
|
+
return JSON.parse(JSON.stringify(value));
|
|
2941
|
+
} catch {
|
|
2942
|
+
return String(value);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
function prettyPrintHtml(html) {
|
|
2946
|
+
return html.replace(/>(?=<)/g, ">\n");
|
|
2947
|
+
}
|
|
2948
|
+
//#endregion
|
|
2949
|
+
//#region src/modules/browser/TabRegistry.ts
|
|
2950
|
+
/**
|
|
2951
|
+
* TabRegistry — unified tab/page state for all browser tools.
|
|
2952
|
+
*
|
|
2953
|
+
* Replaces the fragmented model where tab_workflow kept its own alias→index map
|
|
2954
|
+
* and browser_control used collector.listPages()/selectPage() independently.
|
|
2955
|
+
*
|
|
2956
|
+
* Key properties:
|
|
2957
|
+
* - pageId is session-stable (survives tab open/close of other tabs)
|
|
2958
|
+
* - Alias binds to pageId, not index
|
|
2959
|
+
* - Stale detection when a previously registered page disappears
|
|
2960
|
+
* - Single source of truth for "current page"
|
|
2961
|
+
*/
|
|
2962
|
+
let globalIdCounter = 0;
|
|
2963
|
+
var TabRegistry = class {
|
|
2964
|
+
pageIdByHandle = /* @__PURE__ */ new WeakMap();
|
|
2965
|
+
tabsById = /* @__PURE__ */ new Map();
|
|
2966
|
+
aliasToPageId = /* @__PURE__ */ new Map();
|
|
2967
|
+
currentPageId = null;
|
|
2968
|
+
sharedContext = /* @__PURE__ */ new Map();
|
|
2969
|
+
/**
|
|
2970
|
+
* Register a page and get a stable pageId.
|
|
2971
|
+
* If the page object was previously registered, returns its existing pageId.
|
|
2972
|
+
*/
|
|
2973
|
+
registerPage(page, meta) {
|
|
2974
|
+
const handle = page;
|
|
2975
|
+
const existingId = this.pageIdByHandle.get(handle);
|
|
2976
|
+
if (existingId) {
|
|
2977
|
+
const existing = this.tabsById.get(existingId);
|
|
2978
|
+
if (existing) {
|
|
2979
|
+
existing.meta = meta;
|
|
2980
|
+
existing.stale = false;
|
|
2981
|
+
} else this.tabsById.set(existingId, {
|
|
2982
|
+
page,
|
|
2983
|
+
meta,
|
|
2984
|
+
stale: false
|
|
2985
|
+
});
|
|
2986
|
+
return existingId;
|
|
2987
|
+
}
|
|
2988
|
+
globalIdCounter += 1;
|
|
2989
|
+
const pageId = `tab-${globalIdCounter}`;
|
|
2990
|
+
this.pageIdByHandle.set(handle, pageId);
|
|
2991
|
+
this.tabsById.set(pageId, {
|
|
2992
|
+
page,
|
|
2993
|
+
meta,
|
|
2994
|
+
stale: false
|
|
2995
|
+
});
|
|
2996
|
+
logger.debug(`[TabRegistry] Registered page ${pageId} (index=${meta.index}, url=${meta.url})`);
|
|
2997
|
+
return pageId;
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Reconcile the registry with a fresh pages list.
|
|
3001
|
+
* - New pages get registered
|
|
3002
|
+
* - Missing pages get marked stale
|
|
3003
|
+
* - Index updates are applied
|
|
3004
|
+
* Returns the full tab list.
|
|
3005
|
+
*/
|
|
3006
|
+
reconcilePages(pages, metaList) {
|
|
3007
|
+
const activeIds = /* @__PURE__ */ new Set();
|
|
3008
|
+
for (let i = 0; i < pages.length; i++) {
|
|
3009
|
+
const page = pages[i];
|
|
3010
|
+
const meta = metaList[i] ?? {
|
|
3011
|
+
index: i,
|
|
3012
|
+
url: "",
|
|
3013
|
+
title: ""
|
|
3014
|
+
};
|
|
3015
|
+
const pageId = this.registerPage(page, {
|
|
3016
|
+
...meta,
|
|
3017
|
+
index: i
|
|
3018
|
+
});
|
|
3019
|
+
activeIds.add(pageId);
|
|
3020
|
+
}
|
|
3021
|
+
for (const [pageId, entry] of this.tabsById) if (!activeIds.has(pageId) && !entry.stale) {
|
|
3022
|
+
entry.stale = true;
|
|
3023
|
+
logger.debug(`[TabRegistry] Page ${pageId} marked stale`);
|
|
3024
|
+
}
|
|
3025
|
+
if (this.currentPageId && !activeIds.has(this.currentPageId)) {
|
|
3026
|
+
logger.debug(`[TabRegistry] Current page ${this.currentPageId} is stale, clearing`);
|
|
3027
|
+
this.currentPageId = null;
|
|
3028
|
+
}
|
|
3029
|
+
return this.listTabs();
|
|
3030
|
+
}
|
|
3031
|
+
/** Bind an alias to a pageId. */
|
|
3032
|
+
bindAlias(alias, pageId) {
|
|
3033
|
+
if (!this.tabsById.has(pageId)) return false;
|
|
3034
|
+
this.aliasToPageId.set(alias, pageId);
|
|
3035
|
+
return true;
|
|
3036
|
+
}
|
|
3037
|
+
/** Bind an alias to a page by its current index. */
|
|
3038
|
+
bindAliasByIndex(alias, index) {
|
|
3039
|
+
for (const [pageId, entry] of this.tabsById) if (entry.meta.index === index && !entry.stale) {
|
|
3040
|
+
this.aliasToPageId.set(alias, pageId);
|
|
3041
|
+
return pageId;
|
|
3042
|
+
}
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
/** Remove an alias binding. */
|
|
3046
|
+
unbindAlias(alias) {
|
|
3047
|
+
return this.aliasToPageId.delete(alias);
|
|
3048
|
+
}
|
|
3049
|
+
/** Resolve an alias to its pageId. Returns null if alias not found or page is stale. */
|
|
3050
|
+
resolveAlias(alias) {
|
|
3051
|
+
const pageId = this.aliasToPageId.get(alias);
|
|
3052
|
+
if (!pageId) return null;
|
|
3053
|
+
const entry = this.tabsById.get(pageId);
|
|
3054
|
+
if (!entry || entry.stale) return null;
|
|
3055
|
+
return pageId;
|
|
3056
|
+
}
|
|
3057
|
+
/** Get the page object by pageId. Returns null if not found or stale. */
|
|
3058
|
+
getPageById(pageId) {
|
|
3059
|
+
const entry = this.tabsById.get(pageId);
|
|
3060
|
+
if (!entry || entry.stale) return null;
|
|
3061
|
+
return entry.page;
|
|
3062
|
+
}
|
|
3063
|
+
/** Get full tab descriptor by pageId. */
|
|
3064
|
+
getTabById(pageId) {
|
|
3065
|
+
const entry = this.tabsById.get(pageId);
|
|
3066
|
+
if (!entry) return null;
|
|
3067
|
+
const aliases = this.getAliasesForPageId(pageId);
|
|
3068
|
+
return {
|
|
3069
|
+
pageId,
|
|
3070
|
+
index: entry.meta.index,
|
|
3071
|
+
url: entry.meta.url,
|
|
3072
|
+
title: entry.meta.title,
|
|
3073
|
+
page: entry.page,
|
|
3074
|
+
aliases,
|
|
3075
|
+
stale: entry.stale
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
/** Get tab by current index. */
|
|
3079
|
+
getTabByIndex(index) {
|
|
3080
|
+
for (const [pageId, entry] of this.tabsById) if (entry.meta.index === index && !entry.stale) return this.getTabById(pageId);
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
/** Find a tab matching a predicate. */
|
|
3084
|
+
findTab(predicate) {
|
|
3085
|
+
for (const [pageId] of this.tabsById) {
|
|
3086
|
+
const tab = this.getTabById(pageId);
|
|
3087
|
+
if (tab && predicate(tab)) return tab;
|
|
3088
|
+
}
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
/** Set the current page by pageId. */
|
|
3092
|
+
setCurrentPageId(pageId) {
|
|
3093
|
+
const entry = this.tabsById.get(pageId);
|
|
3094
|
+
if (!entry || entry.stale) return false;
|
|
3095
|
+
this.currentPageId = pageId;
|
|
3096
|
+
return true;
|
|
3097
|
+
}
|
|
3098
|
+
/** Set the current page by index. Returns the tab descriptor or null. */
|
|
3099
|
+
setCurrentByIndex(index) {
|
|
3100
|
+
const tab = this.getTabByIndex(index);
|
|
3101
|
+
if (tab) this.currentPageId = tab.pageId;
|
|
3102
|
+
return tab;
|
|
3103
|
+
}
|
|
3104
|
+
/** Get the current pageId. */
|
|
3105
|
+
getCurrentPageId() {
|
|
3106
|
+
return this.currentPageId;
|
|
3107
|
+
}
|
|
3108
|
+
/** Get current page object. */
|
|
3109
|
+
getCurrentPage() {
|
|
3110
|
+
if (!this.currentPageId) return null;
|
|
3111
|
+
return this.getPageById(this.currentPageId);
|
|
3112
|
+
}
|
|
3113
|
+
/** Get full info about the current tab, suitable for tool responses. */
|
|
3114
|
+
getCurrentTabInfo(driver) {
|
|
3115
|
+
const allAliases = [];
|
|
3116
|
+
const staleAliases = [];
|
|
3117
|
+
for (const [alias, pageId] of this.aliasToPageId) {
|
|
3118
|
+
const entry = this.tabsById.get(pageId);
|
|
3119
|
+
const stale = !entry || entry.stale;
|
|
3120
|
+
allAliases.push({
|
|
3121
|
+
alias,
|
|
3122
|
+
pageId,
|
|
3123
|
+
index: entry?.meta.index ?? null,
|
|
3124
|
+
stale
|
|
3125
|
+
});
|
|
3126
|
+
if (stale) staleAliases.push(alias);
|
|
3127
|
+
}
|
|
3128
|
+
const currentEntry = this.currentPageId ? this.tabsById.get(this.currentPageId) : null;
|
|
3129
|
+
const current = currentEntry && !currentEntry.stale ? currentEntry : null;
|
|
3130
|
+
return {
|
|
3131
|
+
driver,
|
|
3132
|
+
currentPageId: current ? this.currentPageId : null,
|
|
3133
|
+
currentIndex: current?.meta.index ?? null,
|
|
3134
|
+
url: current?.meta.url ?? null,
|
|
3135
|
+
title: current?.meta.title ?? null,
|
|
3136
|
+
aliases: allAliases,
|
|
3137
|
+
staleAliases
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
/** Get a compact context snapshot for tool response enrichment. */
|
|
3141
|
+
getContextMeta() {
|
|
3142
|
+
const currentEntry = this.currentPageId ? this.tabsById.get(this.currentPageId) : null;
|
|
3143
|
+
const current = currentEntry && !currentEntry.stale ? currentEntry : null;
|
|
3144
|
+
return {
|
|
3145
|
+
url: current?.meta.url ?? null,
|
|
3146
|
+
title: current?.meta.title ?? null,
|
|
3147
|
+
tabIndex: current?.meta.index ?? null,
|
|
3148
|
+
pageId: current ? this.currentPageId : null
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
/** List all non-stale tabs. */
|
|
3152
|
+
listTabs() {
|
|
3153
|
+
const result = [];
|
|
3154
|
+
for (const [pageId, entry] of this.tabsById) if (!entry.stale) {
|
|
3155
|
+
const aliases = this.getAliasesForPageId(pageId);
|
|
3156
|
+
result.push({
|
|
3157
|
+
pageId,
|
|
3158
|
+
index: entry.meta.index,
|
|
3159
|
+
url: entry.meta.url,
|
|
3160
|
+
title: entry.meta.title,
|
|
3161
|
+
page: entry.page,
|
|
3162
|
+
aliases,
|
|
3163
|
+
stale: false
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
return result.toSorted((a, b) => a.index - b.index);
|
|
3167
|
+
}
|
|
3168
|
+
/** List all tabs including stale ones. */
|
|
3169
|
+
listAllTabs() {
|
|
3170
|
+
const result = [];
|
|
3171
|
+
for (const [pageId, entry] of this.tabsById) {
|
|
3172
|
+
const aliases = this.getAliasesForPageId(pageId);
|
|
3173
|
+
result.push({
|
|
3174
|
+
pageId,
|
|
3175
|
+
index: entry.meta.index,
|
|
3176
|
+
url: entry.meta.url,
|
|
3177
|
+
title: entry.meta.title,
|
|
3178
|
+
page: entry.page,
|
|
3179
|
+
aliases,
|
|
3180
|
+
stale: entry.stale
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
return result.toSorted((a, b) => a.index - b.index);
|
|
3184
|
+
}
|
|
3185
|
+
setSharedContext(key, value) {
|
|
3186
|
+
this.sharedContext.set(key, value);
|
|
3187
|
+
}
|
|
3188
|
+
getSharedContext(key) {
|
|
3189
|
+
return {
|
|
3190
|
+
value: this.sharedContext.get(key) ?? null,
|
|
3191
|
+
found: this.sharedContext.has(key)
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
getSharedContextMap() {
|
|
3195
|
+
const result = {};
|
|
3196
|
+
this.sharedContext.forEach((v, k) => {
|
|
3197
|
+
result[k] = v;
|
|
3198
|
+
});
|
|
3199
|
+
return result;
|
|
3200
|
+
}
|
|
3201
|
+
/** Clear all state. */
|
|
3202
|
+
clear() {
|
|
3203
|
+
this.tabsById.clear();
|
|
3204
|
+
this.aliasToPageId.clear();
|
|
3205
|
+
this.sharedContext.clear();
|
|
3206
|
+
this.currentPageId = null;
|
|
3207
|
+
}
|
|
3208
|
+
getAliasesForPageId(pageId) {
|
|
3209
|
+
const aliases = [];
|
|
3210
|
+
for (const [alias, pid] of this.aliasToPageId) if (pid === pageId) aliases.push(alias);
|
|
3211
|
+
return aliases;
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
//#endregion
|
|
3215
|
+
//#region src/server/domains/browser/handlers/facade-initializer.ts
|
|
3216
|
+
function initializeBrowserHandlerModules(deps) {
|
|
3217
|
+
const commonDeps = {
|
|
3218
|
+
getActiveDriver: deps.getActiveDriver,
|
|
3219
|
+
getCamoufoxPage: deps.getCamoufoxPage
|
|
3220
|
+
};
|
|
3221
|
+
const tabRegistry = new TabRegistry();
|
|
3222
|
+
const targetControl = new TargetControlHandlers({
|
|
3223
|
+
collector: deps.collector,
|
|
3224
|
+
consoleMonitor: deps.consoleMonitor,
|
|
3225
|
+
getTabRegistry: () => tabRegistry
|
|
3226
|
+
});
|
|
3227
|
+
return {
|
|
3228
|
+
tabRegistry,
|
|
3229
|
+
targetControl,
|
|
3230
|
+
browserControl: new BrowserControlHandlers({
|
|
3231
|
+
collector: deps.collector,
|
|
3232
|
+
pageController: deps.pageController,
|
|
3233
|
+
consoleMonitor: deps.consoleMonitor,
|
|
3234
|
+
getActiveDriver: deps.getActiveDriver,
|
|
3235
|
+
getCamoufoxManager: deps.getCamoufoxManager,
|
|
3236
|
+
getCamoufoxPage: deps.getCamoufoxPage,
|
|
3237
|
+
getTabRegistry: () => tabRegistry,
|
|
3238
|
+
clearAttachedTargetContext: (context) => targetControl.clearAttachedTargetContext(context)
|
|
3239
|
+
}),
|
|
3240
|
+
camoufoxBrowser: new CamoufoxBrowserHandlers({
|
|
3241
|
+
getCamoufoxManager: deps.getCamoufoxManager,
|
|
3242
|
+
setCamoufoxManager: deps.setCamoufoxManager,
|
|
3243
|
+
closeCamoufox: deps.closeCamoufox
|
|
3244
|
+
}),
|
|
3245
|
+
pageNavigation: new PageNavigationHandlers({
|
|
3246
|
+
pageController: deps.pageController,
|
|
3247
|
+
consoleMonitor: deps.consoleMonitor,
|
|
3248
|
+
eventBus: deps.eventBus,
|
|
3249
|
+
...commonDeps
|
|
3250
|
+
}),
|
|
3251
|
+
pageInteraction: new PageInteractionHandlers({
|
|
3252
|
+
pageController: deps.pageController,
|
|
3253
|
+
...commonDeps
|
|
3254
|
+
}),
|
|
3255
|
+
pageEvaluation: new PageEvaluationHandlers({
|
|
3256
|
+
pageController: deps.pageController,
|
|
3257
|
+
detailedDataManager: deps.detailedDataManager,
|
|
3258
|
+
...commonDeps
|
|
3259
|
+
}),
|
|
3260
|
+
targetEvaluation: new TargetEvaluationHandlers({
|
|
3261
|
+
pageController: deps.pageController,
|
|
3262
|
+
detailedDataManager: deps.detailedDataManager
|
|
3263
|
+
}),
|
|
3264
|
+
pageData: new PageDataHandlers({
|
|
3265
|
+
pageController: deps.pageController,
|
|
3266
|
+
...commonDeps
|
|
3267
|
+
}),
|
|
3268
|
+
consoleHandlers: new ConsoleHandlers({
|
|
3269
|
+
consoleMonitor: deps.consoleMonitor,
|
|
3270
|
+
detailedDataManager: deps.detailedDataManager
|
|
3271
|
+
}),
|
|
3272
|
+
scriptManagement: new ScriptManagementHandlers({
|
|
3273
|
+
scriptManager: deps.scriptManager,
|
|
3274
|
+
detailedDataManager: deps.detailedDataManager
|
|
3275
|
+
}),
|
|
3276
|
+
captchaHandlers: new CaptchaHandlers({
|
|
3277
|
+
pageController: deps.pageController,
|
|
3278
|
+
captchaDetector: deps.captchaDetector,
|
|
3279
|
+
autoDetectCaptcha: deps.getAutoDetectCaptcha(),
|
|
3280
|
+
autoSwitchHeadless: deps.getAutoSwitchHeadless(),
|
|
3281
|
+
captchaTimeout: deps.getCaptchaTimeout(),
|
|
3282
|
+
setAutoDetectCaptcha: deps.setAutoDetectCaptcha,
|
|
3283
|
+
setAutoSwitchHeadless: deps.setAutoSwitchHeadless,
|
|
3284
|
+
setCaptchaTimeout: deps.setCaptchaTimeout
|
|
3285
|
+
}),
|
|
3286
|
+
stealthInjection: new StealthInjectionHandlers({
|
|
3287
|
+
pageController: deps.pageController,
|
|
3288
|
+
...commonDeps
|
|
3289
|
+
}),
|
|
3290
|
+
frameworkState: new FrameworkStateHandlers({ getActivePage: () => deps.collector.getActivePage() }),
|
|
3291
|
+
indexedDBDump: new IndexedDBDumpHandlers({ getActivePage: () => deps.collector.getActivePage() }),
|
|
3292
|
+
jsHeapSearch: new JSHeapSearchHandlers({
|
|
3293
|
+
getActivePage: () => deps.collector.getActivePage(),
|
|
3294
|
+
getActiveDriver: deps.getActiveDriver
|
|
3295
|
+
}),
|
|
3296
|
+
tabWorkflow: new TabWorkflowHandlers({
|
|
3297
|
+
getActiveDriver: deps.getActiveDriver,
|
|
3298
|
+
getCamoufoxPage: deps.getCamoufoxPage,
|
|
3299
|
+
getPageController: () => deps.pageController,
|
|
3300
|
+
getTabRegistry: () => tabRegistry
|
|
3301
|
+
}),
|
|
3302
|
+
detailedData: new DetailedDataHandlers({ detailedDataManager: deps.detailedDataManager }),
|
|
3303
|
+
jsdomHandlers: new JsdomHandlers()
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
//#endregion
|
|
3307
|
+
//#region src/server/domains/browser/handlers/human-behavior.ts
|
|
3308
|
+
/** Cubic Bezier: P(t) = (1-t)^3·P0 + 3(1-t)^2·t·P1 + 3(1-t)·t^2·P2 + t^3·P3 */
|
|
3309
|
+
function cubicBezier(p0, p1, p2, p3, t) {
|
|
3310
|
+
const u = 1 - t;
|
|
3311
|
+
const uu = u * u;
|
|
3312
|
+
const uuu = uu * u;
|
|
3313
|
+
const tt = t * t;
|
|
3314
|
+
const ttt = tt * t;
|
|
3315
|
+
return {
|
|
3316
|
+
x: uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x,
|
|
3317
|
+
y: uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
/** Generate control points with lateral offset for natural curve feel. */
|
|
3321
|
+
function generateControlPoints(from, to) {
|
|
3322
|
+
const dx = to.x - from.x;
|
|
3323
|
+
const dy = to.y - from.y;
|
|
3324
|
+
const perpX = -dy;
|
|
3325
|
+
const perpY = dx;
|
|
3326
|
+
const len = Math.sqrt(perpX * perpX + perpY * perpY) || 1;
|
|
3327
|
+
const offset1 = (Math.random() - .5) * .4;
|
|
3328
|
+
const offset2 = (Math.random() - .5) * .4;
|
|
3329
|
+
return [{
|
|
3330
|
+
x: from.x + dx * .3 + perpX / len * Math.abs(dx + dy) * offset1,
|
|
3331
|
+
y: from.y + dy * .3 + perpY / len * Math.abs(dx + dy) * offset1
|
|
3332
|
+
}, {
|
|
3333
|
+
x: from.x + dx * .7 + perpX / len * Math.abs(dx + dy) * offset2,
|
|
3334
|
+
y: from.y + dy * .7 + perpY / len * Math.abs(dx + dy) * offset2
|
|
3335
|
+
}];
|
|
3336
|
+
}
|
|
3337
|
+
/** Easing functions for speed curves. */
|
|
3338
|
+
function easeT(t, curve) {
|
|
3339
|
+
switch (curve) {
|
|
3340
|
+
case "linear": return t;
|
|
3341
|
+
case "ease-in": return t * t;
|
|
3342
|
+
case "ease-out": return 1 - (1 - t) * (1 - t);
|
|
3343
|
+
default: return t < .5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
function sleep$1(ms) {
|
|
3347
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3348
|
+
}
|
|
3349
|
+
async function handleHumanMouse(args, collector) {
|
|
3350
|
+
try {
|
|
3351
|
+
const page = await collector.getActivePage();
|
|
3352
|
+
if (!page) return R.fail("No active page. Use browser_launch or browser_attach first.").build();
|
|
3353
|
+
let toX = argNumber(args, "toX");
|
|
3354
|
+
let toY = argNumber(args, "toY");
|
|
3355
|
+
const selector = argString(args, "selector");
|
|
3356
|
+
if (selector) {
|
|
3357
|
+
const box = await page.evaluate((sel) => {
|
|
3358
|
+
const el = document.querySelector(sel);
|
|
3359
|
+
if (!el) return null;
|
|
3360
|
+
const rect = el.getBoundingClientRect();
|
|
3361
|
+
return {
|
|
3362
|
+
x: rect.x + rect.width / 2,
|
|
3363
|
+
y: rect.y + rect.height / 2
|
|
3364
|
+
};
|
|
3365
|
+
}, selector);
|
|
3366
|
+
if (!box) return R.fail(`Selector not found: ${selector}`).build();
|
|
3367
|
+
toX = box.x;
|
|
3368
|
+
toY = box.y;
|
|
3369
|
+
}
|
|
3370
|
+
if (toX === void 0 || toY === void 0) return R.fail("Either selector or toX/toY coordinates are required").build();
|
|
3371
|
+
const fromX = argNumber(args, "fromX", 0);
|
|
3372
|
+
const fromY = argNumber(args, "fromY", 0);
|
|
3373
|
+
const steps = Math.max(1, Math.min(argNumber(args, "steps", 24), 500));
|
|
3374
|
+
const durationMs = Math.max(10, Math.min(argNumber(args, "durationMs", 600), 3e4));
|
|
3375
|
+
const jitterPx = Math.max(0, Math.min(argNumber(args, "jitterPx", 1.5), 20));
|
|
3376
|
+
const curve = argString(args, "curve", "ease");
|
|
3377
|
+
const shouldClick = argBool(args, "click", false);
|
|
3378
|
+
const from = {
|
|
3379
|
+
x: fromX,
|
|
3380
|
+
y: fromY
|
|
3381
|
+
};
|
|
3382
|
+
const to = {
|
|
3383
|
+
x: toX,
|
|
3384
|
+
y: toY
|
|
3385
|
+
};
|
|
3386
|
+
const [cp1, cp2] = generateControlPoints(from, to);
|
|
3387
|
+
const stepDelay = durationMs / steps;
|
|
3388
|
+
for (let i = 0; i <= steps; i++) {
|
|
3389
|
+
const pt = cubicBezier(from, cp1, cp2, to, easeT(i / steps, curve));
|
|
3390
|
+
if (i > 0 && i < steps) {
|
|
3391
|
+
pt.x += (Math.random() - .5) * 2 * jitterPx;
|
|
3392
|
+
pt.y += (Math.random() - .5) * 2 * jitterPx;
|
|
3393
|
+
}
|
|
3394
|
+
pt.x = Math.max(0, pt.x);
|
|
3395
|
+
pt.y = Math.max(0, pt.y);
|
|
3396
|
+
await page.mouse.move(pt.x, pt.y);
|
|
3397
|
+
await sleep$1(stepDelay * (.8 + Math.random() * .4));
|
|
3398
|
+
}
|
|
3399
|
+
if (shouldClick) await page.mouse.click(toX, toY);
|
|
3400
|
+
return R.ok().build({
|
|
3401
|
+
from: {
|
|
3402
|
+
x: fromX,
|
|
3403
|
+
y: fromY
|
|
3404
|
+
},
|
|
3405
|
+
to: {
|
|
3406
|
+
x: toX,
|
|
3407
|
+
y: toY
|
|
3408
|
+
},
|
|
3409
|
+
steps,
|
|
3410
|
+
durationMs,
|
|
3411
|
+
clicked: shouldClick
|
|
3412
|
+
});
|
|
3413
|
+
} catch (e) {
|
|
3414
|
+
return R.fail(e).build();
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
async function handleHumanScroll(args, collector) {
|
|
3418
|
+
try {
|
|
3419
|
+
const page = await collector.getActivePage();
|
|
3420
|
+
if (!page) return R.fail("No active page.").build();
|
|
3421
|
+
const distance = Math.max(1, Math.min(argNumber(args, "distance", 500), 1e4));
|
|
3422
|
+
const direction = argString(args, "direction", "down");
|
|
3423
|
+
const durationMs = Math.max(10, Math.min(argNumber(args, "durationMs", 1500), 3e4));
|
|
3424
|
+
const segments = Math.max(1, Math.min(argNumber(args, "segments", 8), 200));
|
|
3425
|
+
const pauseMs = typeof args["pauseMs"] === "number" && Number.isFinite(args["pauseMs"]) ? Math.max(0, Math.min(argNumber(args, "pauseMs", 80), 5e3)) : Math.max(0, Math.min(Math.round(durationMs / segments), 5e3));
|
|
3426
|
+
const jitter = Math.max(0, Math.min(argNumber(args, "jitter", .3), 1));
|
|
3427
|
+
const selector = argString(args, "selector");
|
|
3428
|
+
const isVertical = direction === "up" || direction === "down";
|
|
3429
|
+
const sign = direction === "down" || direction === "right" ? 1 : -1;
|
|
3430
|
+
let scrolled = 0;
|
|
3431
|
+
for (let i = 0; i < segments; i++) {
|
|
3432
|
+
const decel = 1 - i / segments * .4;
|
|
3433
|
+
const segmentDist = distance / segments * decel * (1 + (Math.random() - .5) * jitter * 2);
|
|
3434
|
+
const actualDist = Math.min(segmentDist, distance - scrolled);
|
|
3435
|
+
if (actualDist <= 0) break;
|
|
3436
|
+
const deltaX = isVertical ? 0 : actualDist * sign;
|
|
3437
|
+
const deltaY = isVertical ? actualDist * sign : 0;
|
|
3438
|
+
if (selector) await page.evaluate((sel, dx, dy) => {
|
|
3439
|
+
const el = document.querySelector(sel);
|
|
3440
|
+
if (el) el.scrollBy({
|
|
3441
|
+
left: dx,
|
|
3442
|
+
top: dy,
|
|
3443
|
+
behavior: "auto"
|
|
3444
|
+
});
|
|
3445
|
+
}, selector, deltaX, deltaY);
|
|
3446
|
+
else await page.evaluate((dx, dy) => window.scrollBy({
|
|
3447
|
+
left: dx,
|
|
3448
|
+
top: dy,
|
|
3449
|
+
behavior: "auto"
|
|
3450
|
+
}), deltaX, deltaY);
|
|
3451
|
+
scrolled += actualDist;
|
|
3452
|
+
await sleep$1(pauseMs * (.5 + Math.random()));
|
|
3453
|
+
}
|
|
3454
|
+
return R.ok().build({
|
|
3455
|
+
direction,
|
|
3456
|
+
requestedDistance: distance,
|
|
3457
|
+
actualScrolled: Math.round(scrolled),
|
|
3458
|
+
durationMs,
|
|
3459
|
+
pauseMs,
|
|
3460
|
+
segments
|
|
3461
|
+
});
|
|
3462
|
+
} catch (e) {
|
|
3463
|
+
return R.fail(e).build();
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
async function handleHumanTyping(args, collector) {
|
|
3467
|
+
try {
|
|
3468
|
+
const page = await collector.getActivePage();
|
|
3469
|
+
if (!page) return R.fail("No active page.").build();
|
|
3470
|
+
const selector = argString(args, "selector", "");
|
|
3471
|
+
const text = argString(args, "text", "");
|
|
3472
|
+
const wpm = Math.max(10, Math.min(argNumber(args, "wpm", 90), 300));
|
|
3473
|
+
const errorRate = Math.max(0, Math.min(argNumber(args, "errorRate", .02), .3));
|
|
3474
|
+
const correctDelayMs = Math.max(50, Math.min(argNumber(args, "correctDelayMs", 200), 2e3));
|
|
3475
|
+
const clearFirst = argBool(args, "clearFirst", false);
|
|
3476
|
+
if (!selector || !text) return R.fail("selector and text are required").build();
|
|
3477
|
+
const avgDelayMs = 6e4 / (wpm * 5);
|
|
3478
|
+
await page.click(selector);
|
|
3479
|
+
if (clearFirst) await page.evaluate((sel) => {
|
|
3480
|
+
const el = document.querySelector(sel);
|
|
3481
|
+
if (el) el.value = "";
|
|
3482
|
+
}, selector);
|
|
3483
|
+
let typoCount = 0;
|
|
3484
|
+
for (const char of text) {
|
|
3485
|
+
if (Math.random() < errorRate && char !== " ") {
|
|
3486
|
+
const wrongChar = String.fromCharCode(char.charCodeAt(0) + (Math.random() > .5 ? 1 : -1));
|
|
3487
|
+
await page.keyboard.type(wrongChar, { delay: 0 });
|
|
3488
|
+
await sleep$1(correctDelayMs * (.5 + Math.random()));
|
|
3489
|
+
await page.keyboard.press("Backspace");
|
|
3490
|
+
await sleep$1(50 + Math.random() * 50);
|
|
3491
|
+
typoCount++;
|
|
3492
|
+
}
|
|
3493
|
+
await page.keyboard.type(char, { delay: 0 });
|
|
3494
|
+
let delay = avgDelayMs * (.5 + Math.random());
|
|
3495
|
+
if (char === " " || ".,:;!?".includes(char)) delay *= 1.5 + Math.random() * .5;
|
|
3496
|
+
await sleep$1(delay);
|
|
3497
|
+
}
|
|
3498
|
+
return R.ok().build({
|
|
3499
|
+
selector,
|
|
3500
|
+
length: text.length,
|
|
3501
|
+
wpm,
|
|
3502
|
+
typosSimulated: typoCount,
|
|
3503
|
+
errorRate
|
|
3504
|
+
});
|
|
3505
|
+
} catch (e) {
|
|
3506
|
+
return R.fail(e).build();
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
//#endregion
|
|
3510
|
+
//#region src/server/domains/browser/handlers/captcha-solver.ts
|
|
3511
|
+
function sleep(ms) {
|
|
3512
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3513
|
+
}
|
|
3514
|
+
function normalizeSolverMode(rawMode) {
|
|
3515
|
+
const value = typeof rawMode === "string" ? rawMode.toLowerCase() : "";
|
|
3516
|
+
if (value === "hook") return "hook";
|
|
3517
|
+
if (value === "external_service") return "external_service";
|
|
3518
|
+
if (value === "2captcha" || value === "anticaptcha" || value === "capsolver") return "external_service";
|
|
3519
|
+
return "manual";
|
|
3520
|
+
}
|
|
3521
|
+
function normalizeChallengeTypeHint(rawType) {
|
|
3522
|
+
const value = typeof rawType === "string" ? rawType.toLowerCase() : "";
|
|
3523
|
+
if (value === "image") return "image";
|
|
3524
|
+
if (value === "widget" || value === "recaptcha_v2" || value === "recaptcha_v3" || value === "hcaptcha" || value === "funcaptcha" || value === "turnstile") return "widget";
|
|
3525
|
+
if (value === "browser_check" || value === "managed_widget") return "browser_check";
|
|
3526
|
+
return "auto";
|
|
3527
|
+
}
|
|
3528
|
+
function resolveLegacyServiceOverride(rawProvider) {
|
|
3529
|
+
if (typeof rawProvider !== "string" || !rawProvider.trim()) return;
|
|
3530
|
+
return rawProvider.trim().toLowerCase();
|
|
3531
|
+
}
|
|
3532
|
+
function resolveExternalServiceName(args) {
|
|
3533
|
+
const legacyOverride = resolveLegacyServiceOverride(args.provider);
|
|
3534
|
+
const configured = (process.env.CAPTCHA_PROVIDER || "").trim().toLowerCase();
|
|
3535
|
+
return legacyOverride || configured || "2captcha";
|
|
3536
|
+
}
|
|
3537
|
+
async function solveWith2Captcha(apiKey, params, timeoutMs) {
|
|
3538
|
+
const start = Date.now();
|
|
3539
|
+
const baseUrl = CAPTCHA_SOLVER_BASE_URL;
|
|
3540
|
+
if (!baseUrl) throw new Error("CAPTCHA_SOLVER_BASE_URL must be configured before using external_service mode.");
|
|
3541
|
+
const submitBody = {
|
|
3542
|
+
key: apiKey,
|
|
3543
|
+
json: 1
|
|
3544
|
+
};
|
|
3545
|
+
if (params.taskKind === "turnstile" || params.taskKind === "recaptcha_v2" || params.taskKind === "hcaptcha") {
|
|
3546
|
+
submitBody.method = params.taskKind === "turnstile" ? "turnstile" : params.taskKind === "hcaptcha" ? "hcaptcha" : "userrecaptcha";
|
|
3547
|
+
submitBody.sitekey = params.siteKey;
|
|
3548
|
+
submitBody.pageurl = params.pageUrl;
|
|
3549
|
+
} else {
|
|
3550
|
+
submitBody.method = "base64";
|
|
3551
|
+
submitBody.body = params.imageBase64;
|
|
3552
|
+
}
|
|
3553
|
+
const submitData = await (await fetch(`${baseUrl}/in.php`, {
|
|
3554
|
+
method: "POST",
|
|
3555
|
+
headers: { "Content-Type": "application/json" },
|
|
3556
|
+
body: JSON.stringify(submitBody),
|
|
3557
|
+
signal: AbortSignal.timeout(CAPTCHA_SUBMIT_TIMEOUT_MS)
|
|
3558
|
+
})).json();
|
|
3559
|
+
if (submitData.status !== 1) throw new Error(`2captcha submit failed: ${JSON.stringify(submitData)}`);
|
|
3560
|
+
const taskId = submitData.request;
|
|
3561
|
+
const pollInterval = CAPTCHA_POLL_INTERVAL_MS;
|
|
3562
|
+
while (true) {
|
|
3563
|
+
const remaining = timeoutMs - (Date.now() - start);
|
|
3564
|
+
if (remaining <= 0) break;
|
|
3565
|
+
await sleep(Math.min(pollInterval, remaining));
|
|
3566
|
+
if (Date.now() - start >= timeoutMs) break;
|
|
3567
|
+
const resultUrl = new URL(`${baseUrl}/res.php`);
|
|
3568
|
+
resultUrl.searchParams.set("key", apiKey);
|
|
3569
|
+
resultUrl.searchParams.set("action", "get");
|
|
3570
|
+
resultUrl.searchParams.set("id", taskId);
|
|
3571
|
+
resultUrl.searchParams.set("json", "1");
|
|
3572
|
+
const resultData = await (await fetch(resultUrl.toString(), { signal: AbortSignal.timeout(CAPTCHA_RESULT_TIMEOUT_MS) })).json();
|
|
3573
|
+
if (resultData.status === 1) return {
|
|
3574
|
+
token: resultData.request,
|
|
3575
|
+
challengeType: params.taskKind === "image" ? "image" : "widget",
|
|
3576
|
+
mode: "external_service",
|
|
3577
|
+
durationMs: Date.now() - start
|
|
3578
|
+
};
|
|
3579
|
+
if (resultData.request !== "CAPCHA_NOT_READY") throw new Error(`2captcha solve failed: ${JSON.stringify(resultData)}`);
|
|
3580
|
+
}
|
|
3581
|
+
throw new Error(`2captcha solve timeout after ${timeoutMs}ms`);
|
|
3582
|
+
}
|
|
3583
|
+
async function handleCaptchaVisionSolve(args, collector) {
|
|
3584
|
+
const page = await collector.getActivePage();
|
|
3585
|
+
if (!page) return R.fail("No active page.").build();
|
|
3586
|
+
const mode = normalizeSolverMode(args.mode ?? args.provider ?? process.env.CAPTCHA_PROVIDER);
|
|
3587
|
+
const externalService = resolveExternalServiceName(args);
|
|
3588
|
+
const apiKey = argString(args, "apiKey", "") || process.env.CAPTCHA_API_KEY || "";
|
|
3589
|
+
const challengeTypeHint = normalizeChallengeTypeHint(args.challengeType ?? args.typeHint);
|
|
3590
|
+
const timeoutMs = Math.min(Math.max(argNumber(args, "timeoutMs", CAPTCHA_DEFAULT_TIMEOUT_MS), CAPTCHA_MIN_TIMEOUT_MS), CAPTCHA_MAX_TIMEOUT_MS);
|
|
3591
|
+
const maxRetries = Math.min(Math.max(argNumber(args, "maxRetries", CAPTCHA_DEFAULT_RETRIES), 0), CAPTCHA_MAX_RETRIES);
|
|
3592
|
+
let challengeType = challengeTypeHint;
|
|
3593
|
+
let taskKind = challengeTypeHint === "image" ? "image" : "recaptcha_v2";
|
|
3594
|
+
let siteKey = argString(args, "siteKey");
|
|
3595
|
+
const pageUrl = argString(args, "pageUrl", "") || page.url();
|
|
3596
|
+
if (challengeType === "auto") {
|
|
3597
|
+
const detected = await page.evaluate(() => {
|
|
3598
|
+
if (document.querySelector("[data-sitekey]")) {
|
|
3599
|
+
const sk = document.querySelector("[data-sitekey]")?.getAttribute("data-sitekey") || "";
|
|
3600
|
+
if (document.querySelector(".cf-turnstile")) return {
|
|
3601
|
+
challengeType: "widget",
|
|
3602
|
+
taskKind: "turnstile",
|
|
3603
|
+
siteKey: sk
|
|
3604
|
+
};
|
|
3605
|
+
if (document.querySelector(".h-captcha")) return {
|
|
3606
|
+
challengeType: "widget",
|
|
3607
|
+
taskKind: "hcaptcha",
|
|
3608
|
+
siteKey: sk
|
|
3609
|
+
};
|
|
3610
|
+
return {
|
|
3611
|
+
challengeType: "widget",
|
|
3612
|
+
taskKind: "recaptcha_v2",
|
|
3613
|
+
siteKey: sk
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
if (document.querySelector("iframe[src*=\"recaptcha\"]")) return {
|
|
3617
|
+
challengeType: "widget",
|
|
3618
|
+
taskKind: "recaptcha_v2",
|
|
3619
|
+
siteKey: ""
|
|
3620
|
+
};
|
|
3621
|
+
if (document.querySelector("iframe[src*=\"hcaptcha\"]")) return {
|
|
3622
|
+
challengeType: "widget",
|
|
3623
|
+
taskKind: "hcaptcha",
|
|
3624
|
+
siteKey: ""
|
|
3625
|
+
};
|
|
3626
|
+
if (document.querySelector(".cf-turnstile")) return {
|
|
3627
|
+
challengeType: "widget",
|
|
3628
|
+
taskKind: "turnstile",
|
|
3629
|
+
siteKey: ""
|
|
3630
|
+
};
|
|
3631
|
+
return {
|
|
3632
|
+
challengeType: "image",
|
|
3633
|
+
taskKind: "image",
|
|
3634
|
+
siteKey: ""
|
|
3635
|
+
};
|
|
3636
|
+
});
|
|
3637
|
+
challengeType = detected.challengeType;
|
|
3638
|
+
taskKind = detected.taskKind;
|
|
3639
|
+
if (!siteKey && detected.siteKey) siteKey = detected.siteKey;
|
|
3640
|
+
} else if (challengeType === "image") taskKind = "image";
|
|
3641
|
+
else taskKind = "recaptcha_v2";
|
|
3642
|
+
if (mode === "manual") return R.ok().build({
|
|
3643
|
+
mode: "manual",
|
|
3644
|
+
challengeType,
|
|
3645
|
+
siteKey: siteKey ?? null,
|
|
3646
|
+
instruction: "Please solve the CAPTCHA manually in the browser, then continue.",
|
|
3647
|
+
hint: "Configure an external solver service and CAPTCHA_API_KEY to automate this flow."
|
|
3648
|
+
});
|
|
3649
|
+
if (!apiKey) return R.fail("External solver credentials are required. Set CAPTCHA_API_KEY.").build();
|
|
3650
|
+
let lastError = null;
|
|
3651
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
3652
|
+
let result;
|
|
3653
|
+
if (externalService === "2captcha") result = await solveWith2Captcha(apiKey, {
|
|
3654
|
+
taskKind,
|
|
3655
|
+
siteKey,
|
|
3656
|
+
pageUrl
|
|
3657
|
+
}, timeoutMs);
|
|
3658
|
+
else if (externalService === "anticaptcha" || externalService === "capsolver") throw new Error("The selected external solver service is not yet implemented. Currently only the configured primary service and manual mode are supported.");
|
|
3659
|
+
else throw new Error("Unsupported external solver service.");
|
|
3660
|
+
return R.ok().build({
|
|
3661
|
+
token: result.token,
|
|
3662
|
+
challengeType: result.challengeType,
|
|
3663
|
+
mode: result.mode,
|
|
3664
|
+
durationMs: result.durationMs,
|
|
3665
|
+
attempt: attempt + 1
|
|
3666
|
+
});
|
|
3667
|
+
} catch (error) {
|
|
3668
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
3669
|
+
logger.warn(`[captcha] Attempt ${attempt + 1} failed: ${lastError.message}`);
|
|
3670
|
+
}
|
|
3671
|
+
return R.fail(lastError ?? /* @__PURE__ */ new Error("All attempts failed")).merge({
|
|
3672
|
+
challengeType,
|
|
3673
|
+
mode,
|
|
3674
|
+
maxRetries,
|
|
3675
|
+
suggestion: "Try manual mode or adjust the external solver configuration."
|
|
3676
|
+
}).build();
|
|
3677
|
+
}
|
|
3678
|
+
async function handleWidgetChallengeSolve(args, collector) {
|
|
3679
|
+
const page = await collector.getActivePage();
|
|
3680
|
+
if (!page) return R.fail("No active page.").build();
|
|
3681
|
+
const mode = normalizeSolverMode(args.mode ?? args.provider ?? process.env.CAPTCHA_PROVIDER);
|
|
3682
|
+
const externalService = resolveExternalServiceName(args);
|
|
3683
|
+
const apiKey = argString(args, "apiKey", "") || process.env.CAPTCHA_API_KEY || "";
|
|
3684
|
+
const timeoutMs = Math.min(Math.max(argNumber(args, "timeoutMs", 12e4), 5e3), 6e5);
|
|
3685
|
+
const injectToken = argBool(args, "injectToken", true);
|
|
3686
|
+
let siteKey = argString(args, "siteKey");
|
|
3687
|
+
const pageUrl = argString(args, "pageUrl", "") || page.url();
|
|
3688
|
+
if (!siteKey) siteKey = await page.evaluate(() => {
|
|
3689
|
+
return document.querySelector(".cf-turnstile[data-sitekey], [data-sitekey]")?.getAttribute("data-sitekey") ?? "";
|
|
3690
|
+
}) || void 0;
|
|
3691
|
+
if (!siteKey) return R.fail("Could not detect the widget siteKey. Provide it manually or ensure the page exposes a site key.").build();
|
|
3692
|
+
if (mode === "hook") {
|
|
3693
|
+
const hookTimeoutMs = Math.min(timeoutMs, 3e4);
|
|
3694
|
+
const token = await page.evaluate((hookTimeout) => {
|
|
3695
|
+
return new Promise((resolve, reject) => {
|
|
3696
|
+
const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Hook timeout")), hookTimeout);
|
|
3697
|
+
const origCallbacks = window.__turnstile_callbacks;
|
|
3698
|
+
if (origCallbacks) for (const [key, cb] of Object.entries(origCallbacks)) origCallbacks[key] = (captchaToken) => {
|
|
3699
|
+
clearTimeout(timeout);
|
|
3700
|
+
resolve(captchaToken);
|
|
3701
|
+
cb(captchaToken);
|
|
3702
|
+
};
|
|
3703
|
+
else {
|
|
3704
|
+
clearTimeout(timeout);
|
|
3705
|
+
reject(/* @__PURE__ */ new Error("No widget callbacks found. Try external_service mode instead."));
|
|
3706
|
+
}
|
|
3707
|
+
});
|
|
3708
|
+
}, hookTimeoutMs).catch(() => null);
|
|
3709
|
+
if (token) return R.ok().build({
|
|
3710
|
+
token,
|
|
3711
|
+
method: "hook",
|
|
3712
|
+
challengeType: "widget",
|
|
3713
|
+
siteKey
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
if (mode === "manual") return R.ok().build({
|
|
3717
|
+
mode: "manual",
|
|
3718
|
+
challengeType: "widget",
|
|
3719
|
+
siteKey,
|
|
3720
|
+
pageUrl,
|
|
3721
|
+
instruction: "Please complete the widget challenge manually."
|
|
3722
|
+
});
|
|
3723
|
+
if (externalService !== "2captcha") return R.fail("The selected external solver service is not implemented for this widget flow. Currently only the configured primary service, manual mode, and hook mode are supported.").build();
|
|
3724
|
+
if (!apiKey) return R.fail("External solver credentials are required.").build();
|
|
3725
|
+
try {
|
|
3726
|
+
const result = await solveWith2Captcha(apiKey, {
|
|
3727
|
+
taskKind: "turnstile",
|
|
3728
|
+
siteKey,
|
|
3729
|
+
pageUrl
|
|
3730
|
+
}, timeoutMs);
|
|
3731
|
+
if (injectToken && result.token) await page.evaluate((token) => {
|
|
3732
|
+
document.querySelectorAll("input[name*=\"turnstile\"], input[name*=\"cf-turnstile\"]").forEach((input) => {
|
|
3733
|
+
input.value = token;
|
|
3734
|
+
});
|
|
3735
|
+
if (window.turnstile?.getResponse) {}
|
|
3736
|
+
}, result.token);
|
|
3737
|
+
return R.ok().build({
|
|
3738
|
+
token: result.token,
|
|
3739
|
+
challengeType: result.challengeType,
|
|
3740
|
+
siteKey,
|
|
3741
|
+
mode: result.mode,
|
|
3742
|
+
durationMs: result.durationMs,
|
|
3743
|
+
injected: injectToken
|
|
3744
|
+
});
|
|
3745
|
+
} catch (error) {
|
|
3746
|
+
return R.fail(error).merge({
|
|
3747
|
+
siteKey,
|
|
3748
|
+
mode,
|
|
3749
|
+
suggestion: "Try manual mode or hook mode."
|
|
3750
|
+
}).build();
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
//#endregion
|
|
3754
|
+
//#region src/server/domains/browser/handlers/captcha-capabilities.ts
|
|
3755
|
+
function getConfiguredProvider() {
|
|
3756
|
+
return (process.env.CAPTCHA_PROVIDER || "").trim().toLowerCase() || "manual";
|
|
3757
|
+
}
|
|
3758
|
+
function getConfiguredBaseUrl() {
|
|
3759
|
+
return process.env.CAPTCHA_SOLVER_BASE_URL?.trim() || process.env.CAPTCHA_2CAPTCHA_BASE_URL?.trim() || "";
|
|
3760
|
+
}
|
|
3761
|
+
function getTwoCaptchaCapability() {
|
|
3762
|
+
const configuredProvider = getConfiguredProvider();
|
|
3763
|
+
const baseUrl = getConfiguredBaseUrl();
|
|
3764
|
+
const apiKeyConfigured = Boolean(process.env.CAPTCHA_API_KEY?.trim());
|
|
3765
|
+
const baseUrlConfigured = baseUrl.length > 0;
|
|
3766
|
+
const available = apiKeyConfigured && baseUrlConfigured;
|
|
3767
|
+
return {
|
|
3768
|
+
capability: "captcha_external_service_2captcha",
|
|
3769
|
+
status: available ? "available" : "unavailable",
|
|
3770
|
+
reason: available ? void 0 : "The 2captcha-compatible external path needs both CAPTCHA_API_KEY and CAPTCHA_SOLVER_BASE_URL.",
|
|
3771
|
+
fix: available ? void 0 : "Set CAPTCHA_API_KEY and CAPTCHA_SOLVER_BASE_URL to enable external_service mode.",
|
|
3772
|
+
details: {
|
|
3773
|
+
tools: ["captcha_vision_solve", "widget_challenge_solve"],
|
|
3774
|
+
configuredProvider,
|
|
3775
|
+
defaultExternalProviderSupported: configuredProvider === "2captcha",
|
|
3776
|
+
apiKeyConfigured,
|
|
3777
|
+
baseUrlConfigured,
|
|
3778
|
+
...baseUrlConfigured ? { baseUrl } : {}
|
|
3779
|
+
}
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
async function getWidgetHookCapability(collector) {
|
|
3783
|
+
let page;
|
|
3784
|
+
try {
|
|
3785
|
+
page = await collector.getActivePage();
|
|
3786
|
+
} catch (error) {
|
|
3787
|
+
return {
|
|
3788
|
+
capability: "captcha_widget_hook_current_page",
|
|
3789
|
+
status: "unknown",
|
|
3790
|
+
reason: `Current page probe failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3791
|
+
fix: "Attach or launch a browser page before using hook mode.",
|
|
3792
|
+
details: {
|
|
3793
|
+
tools: ["widget_challenge_solve"],
|
|
3794
|
+
pageAttached: false
|
|
3795
|
+
}
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
if (!page) return {
|
|
3799
|
+
capability: "captcha_widget_hook_current_page",
|
|
3800
|
+
status: "unknown",
|
|
3801
|
+
reason: "No active page is attached.",
|
|
3802
|
+
fix: "Attach or launch a browser page before using hook mode.",
|
|
3803
|
+
details: {
|
|
3804
|
+
tools: ["widget_challenge_solve"],
|
|
3805
|
+
pageAttached: false
|
|
3806
|
+
}
|
|
3807
|
+
};
|
|
3808
|
+
try {
|
|
3809
|
+
const probe = await page.evaluate(() => {
|
|
3810
|
+
const callbacksRaw = window.__turnstile_callbacks;
|
|
3811
|
+
const callbackCount = callbacksRaw && typeof callbacksRaw === "object" && !Array.isArray(callbacksRaw) ? Object.keys(callbacksRaw).length : 0;
|
|
3812
|
+
return {
|
|
3813
|
+
url: location.href,
|
|
3814
|
+
callbackCount
|
|
3815
|
+
};
|
|
3816
|
+
});
|
|
3817
|
+
return {
|
|
3818
|
+
capability: "captcha_widget_hook_current_page",
|
|
3819
|
+
status: probe.callbackCount > 0 ? "available" : "unavailable",
|
|
3820
|
+
reason: probe.callbackCount > 0 ? void 0 : "The current page does not expose window.__turnstile_callbacks for hook mode.",
|
|
3821
|
+
fix: probe.callbackCount > 0 ? void 0 : "Use manual mode, or configure the external 2captcha-compatible service for widget solving.",
|
|
3822
|
+
details: {
|
|
3823
|
+
tools: ["widget_challenge_solve"],
|
|
3824
|
+
pageAttached: true,
|
|
3825
|
+
...probe
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3828
|
+
} catch (error) {
|
|
3829
|
+
return {
|
|
3830
|
+
capability: "captcha_widget_hook_current_page",
|
|
3831
|
+
status: "unknown",
|
|
3832
|
+
reason: `Current page probe failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3833
|
+
fix: "Ensure the attached page is reachable before using hook mode.",
|
|
3834
|
+
details: {
|
|
3835
|
+
tools: ["widget_challenge_solve"],
|
|
3836
|
+
pageAttached: true
|
|
3837
|
+
}
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
async function handleCaptchaSolverCapabilities(collector) {
|
|
3842
|
+
const configuredProvider = getConfiguredProvider();
|
|
3843
|
+
const widgetHookCapability = await getWidgetHookCapability(collector);
|
|
3844
|
+
return R.raw(capabilityReport("captcha_solver_capabilities", [
|
|
3845
|
+
{
|
|
3846
|
+
capability: "captcha_manual",
|
|
3847
|
+
status: "available",
|
|
3848
|
+
details: { tools: ["captcha_vision_solve", "widget_challenge_solve"] }
|
|
3849
|
+
},
|
|
3850
|
+
getTwoCaptchaCapability(),
|
|
3851
|
+
{
|
|
3852
|
+
capability: "captcha_external_service_anticaptcha",
|
|
3853
|
+
status: "unavailable",
|
|
3854
|
+
reason: "AntiCaptcha integration is not implemented in this build.",
|
|
3855
|
+
fix: "Use manual mode or the configured 2captcha-compatible service instead.",
|
|
3856
|
+
details: {
|
|
3857
|
+
tools: ["captcha_vision_solve", "widget_challenge_solve"],
|
|
3858
|
+
configuredProvider
|
|
3859
|
+
}
|
|
3860
|
+
},
|
|
3861
|
+
{
|
|
3862
|
+
capability: "captcha_external_service_capsolver",
|
|
3863
|
+
status: "unavailable",
|
|
3864
|
+
reason: "CapSolver integration is not implemented in this build.",
|
|
3865
|
+
fix: "Use manual mode or the configured 2captcha-compatible service instead.",
|
|
3866
|
+
details: {
|
|
3867
|
+
tools: ["captcha_vision_solve", "widget_challenge_solve"],
|
|
3868
|
+
configuredProvider
|
|
3869
|
+
}
|
|
3870
|
+
},
|
|
3871
|
+
widgetHookCapability
|
|
3872
|
+
], { configuredProvider }));
|
|
3873
|
+
}
|
|
3874
|
+
//#endregion
|
|
3875
|
+
//#region src/server/domains/browser/handlers/camoufox-flow.ts
|
|
3876
|
+
function extractCamoufoxConfig(args) {
|
|
3877
|
+
const addons = argStringArray(args, "addons");
|
|
3878
|
+
const excludeAddons = argStringArray(args, "excludeAddons");
|
|
3879
|
+
const fonts = argStringArray(args, "fonts");
|
|
3880
|
+
return {
|
|
3881
|
+
headless: argBool(args, "headless", true),
|
|
3882
|
+
os: argString(args, "os", "windows"),
|
|
3883
|
+
geoip: argBool(args, "geoip", false),
|
|
3884
|
+
humanize: argBool(args, "humanize", false),
|
|
3885
|
+
proxy: argString(args, "proxy") || void 0,
|
|
3886
|
+
blockImages: argBool(args, "blockImages", false),
|
|
3887
|
+
blockWebrtc: argBool(args, "blockWebrtc", false),
|
|
3888
|
+
blockWebgl: argBool(args, "blockWebgl", false),
|
|
3889
|
+
locale: argString(args, "locale") || void 0,
|
|
3890
|
+
addons: addons.length > 0 ? addons : void 0,
|
|
3891
|
+
fonts: fonts.length > 0 ? fonts : void 0,
|
|
3892
|
+
excludeAddons: excludeAddons.length > 0 ? excludeAddons : void 0,
|
|
3893
|
+
customFontsOnly: argBool(args, "customFontsOnly", false),
|
|
3894
|
+
screen: args.screen,
|
|
3895
|
+
window: args.window,
|
|
3896
|
+
fingerprint: argObject(args, "fingerprint"),
|
|
3897
|
+
webglConfig: argObject(args, "webglConfig"),
|
|
3898
|
+
firefoxUserPrefs: argObject(args, "firefoxUserPrefs"),
|
|
3899
|
+
mainWorldEval: argBool(args, "mainWorldEval", false),
|
|
3900
|
+
enableCache: argBool(args, "enableCache", false)
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
async function handleCamoufoxLaunchFlow(context, args) {
|
|
3904
|
+
try {
|
|
3905
|
+
const config = extractCamoufoxConfig(args);
|
|
3906
|
+
if (argString(args, "mode", "launch") === "connect") {
|
|
3907
|
+
const wsEndpoint = argString(args, "wsEndpoint");
|
|
3908
|
+
if (!wsEndpoint) return R.fail("wsEndpoint is required for connect mode.").build();
|
|
3909
|
+
const manager = new CamoufoxBrowserManager(config);
|
|
3910
|
+
await manager.connectToServer(wsEndpoint);
|
|
3911
|
+
context.setCamoufoxManager(manager);
|
|
3912
|
+
context.setActiveDriver("camoufox");
|
|
3913
|
+
context.clearCamoufoxPage();
|
|
3914
|
+
return R.ok().build({
|
|
3915
|
+
driver: "camoufox",
|
|
3916
|
+
mode: "connect",
|
|
3917
|
+
wsEndpoint,
|
|
3918
|
+
message: "Connected to Camoufox server."
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
const manager = new CamoufoxBrowserManager(config);
|
|
3922
|
+
await manager.launch();
|
|
3923
|
+
context.setCamoufoxManager(manager);
|
|
3924
|
+
context.setActiveDriver("camoufox");
|
|
3925
|
+
context.clearCamoufoxPage();
|
|
3926
|
+
return R.ok().build({
|
|
3927
|
+
driver: "camoufox",
|
|
3928
|
+
mode: "launch",
|
|
3929
|
+
config: {
|
|
3930
|
+
os: config.os,
|
|
3931
|
+
headless: config.headless,
|
|
3932
|
+
geoip: config.geoip,
|
|
3933
|
+
humanize: config.humanize,
|
|
3934
|
+
locale: config.locale,
|
|
3935
|
+
blockWebgl: config.blockWebgl,
|
|
3936
|
+
blockImages: config.blockImages,
|
|
3937
|
+
blockWebrtc: config.blockWebrtc
|
|
3938
|
+
},
|
|
3939
|
+
message: "Camoufox (Firefox) browser launched"
|
|
3940
|
+
});
|
|
3941
|
+
} catch (e) {
|
|
3942
|
+
return R.fail(e).build();
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
function normalizeWaitUntil(waitUntil) {
|
|
3946
|
+
if (waitUntil === "networkidle2") return "networkidle";
|
|
3947
|
+
if (waitUntil === "load") return "load";
|
|
3948
|
+
if (waitUntil === "domcontentloaded") return "domcontentloaded";
|
|
3949
|
+
if (waitUntil === "commit") return "commit";
|
|
3950
|
+
return "networkidle";
|
|
3951
|
+
}
|
|
3952
|
+
async function handleCamoufoxNavigateFlow(context, args) {
|
|
3953
|
+
try {
|
|
3954
|
+
const url = argString(args, "url", "");
|
|
3955
|
+
const rawWaitUntil = argString(args, "waitUntil", "networkidle");
|
|
3956
|
+
const timeout = argNumber(args, "timeout");
|
|
3957
|
+
const page = await context.getCamoufoxPage();
|
|
3958
|
+
await page.goto(url, {
|
|
3959
|
+
waitUntil: normalizeWaitUntil(rawWaitUntil),
|
|
3960
|
+
timeout
|
|
3961
|
+
});
|
|
3962
|
+
context.setConsoleMonitorPage(page);
|
|
3963
|
+
return R.ok().build({
|
|
3964
|
+
driver: "camoufox",
|
|
3965
|
+
url: page.url(),
|
|
3966
|
+
title: await page.title()
|
|
3967
|
+
});
|
|
3968
|
+
} catch (e) {
|
|
3969
|
+
return R.fail(e).build();
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
//#endregion
|
|
3973
|
+
//#region src/server/domains/browser/handlers.impl.ts
|
|
3974
|
+
var BrowserToolHandlers = class {
|
|
3975
|
+
collector;
|
|
3976
|
+
pageController;
|
|
3977
|
+
scriptManager;
|
|
3978
|
+
consoleMonitor;
|
|
3979
|
+
captchaDetector;
|
|
3980
|
+
detailedDataManager;
|
|
3981
|
+
camoufoxManager = null;
|
|
3982
|
+
activeDriver = "chrome";
|
|
3983
|
+
camoufoxPage = null;
|
|
3984
|
+
autoDetectCaptcha = true;
|
|
3985
|
+
autoSwitchHeadless = true;
|
|
3986
|
+
captchaTimeout = 3e5;
|
|
3987
|
+
browserControl;
|
|
3988
|
+
targetControl;
|
|
3989
|
+
camoufoxBrowser;
|
|
3990
|
+
pageNavigation;
|
|
3991
|
+
pageInteraction;
|
|
3992
|
+
pageEvaluation;
|
|
3993
|
+
targetEvaluation;
|
|
3994
|
+
pageData;
|
|
3995
|
+
consoleHandlers;
|
|
3996
|
+
scriptManagement;
|
|
3997
|
+
captchaHandlers;
|
|
3998
|
+
stealthInjection;
|
|
3999
|
+
frameworkState;
|
|
4000
|
+
indexedDBDump;
|
|
4001
|
+
jsHeapSearch;
|
|
4002
|
+
tabWorkflow;
|
|
4003
|
+
detailedData;
|
|
4004
|
+
jsdomHandlers;
|
|
4005
|
+
tabRegistry;
|
|
4006
|
+
constructor(collector, pageController, scriptManager, consoleMonitor, eventBus) {
|
|
4007
|
+
this.collector = collector;
|
|
4008
|
+
this.pageController = pageController;
|
|
4009
|
+
this.scriptManager = scriptManager;
|
|
4010
|
+
this.consoleMonitor = consoleMonitor;
|
|
4011
|
+
const screenshotDir = resolveOutputDirectory(getConfig().paths.captchaScreenshotDir, "screenshots/captcha");
|
|
4012
|
+
this.captchaDetector = new AICaptchaDetector(screenshotDir);
|
|
4013
|
+
this.detailedDataManager = DetailedDataManager.getInstance();
|
|
4014
|
+
const modules = initializeBrowserHandlerModules({
|
|
4015
|
+
collector: this.collector,
|
|
4016
|
+
pageController: this.pageController,
|
|
4017
|
+
scriptManager: this.scriptManager,
|
|
4018
|
+
consoleMonitor: this.consoleMonitor,
|
|
4019
|
+
eventBus,
|
|
4020
|
+
captchaDetector: this.captchaDetector,
|
|
4021
|
+
detailedDataManager: this.detailedDataManager,
|
|
4022
|
+
getActiveDriver: () => this.activeDriver,
|
|
4023
|
+
getCamoufoxPage: () => this.getCamoufoxPage(),
|
|
4024
|
+
getCamoufoxManager: () => this.camoufoxManager,
|
|
4025
|
+
setCamoufoxManager: (manager) => {
|
|
4026
|
+
this.camoufoxManager = manager;
|
|
4027
|
+
},
|
|
4028
|
+
closeCamoufox: () => this.closeCamoufox(),
|
|
4029
|
+
getAutoDetectCaptcha: () => this.autoDetectCaptcha,
|
|
4030
|
+
getAutoSwitchHeadless: () => this.autoSwitchHeadless,
|
|
4031
|
+
getCaptchaTimeout: () => this.captchaTimeout,
|
|
4032
|
+
setAutoDetectCaptcha: (value) => {
|
|
4033
|
+
this.autoDetectCaptcha = value;
|
|
4034
|
+
},
|
|
4035
|
+
setAutoSwitchHeadless: (value) => {
|
|
4036
|
+
this.autoSwitchHeadless = value;
|
|
4037
|
+
},
|
|
4038
|
+
setCaptchaTimeout: (value) => {
|
|
4039
|
+
this.captchaTimeout = value;
|
|
4040
|
+
}
|
|
4041
|
+
});
|
|
4042
|
+
this.browserControl = modules.browserControl;
|
|
4043
|
+
this.targetControl = modules.targetControl;
|
|
4044
|
+
this.camoufoxBrowser = modules.camoufoxBrowser;
|
|
4045
|
+
this.pageNavigation = modules.pageNavigation;
|
|
4046
|
+
this.pageInteraction = modules.pageInteraction;
|
|
4047
|
+
this.pageEvaluation = modules.pageEvaluation;
|
|
4048
|
+
this.targetEvaluation = modules.targetEvaluation;
|
|
4049
|
+
this.pageData = modules.pageData;
|
|
4050
|
+
this.consoleHandlers = modules.consoleHandlers;
|
|
4051
|
+
this.scriptManagement = modules.scriptManagement;
|
|
4052
|
+
this.captchaHandlers = modules.captchaHandlers;
|
|
4053
|
+
this.stealthInjection = modules.stealthInjection;
|
|
4054
|
+
this.frameworkState = modules.frameworkState;
|
|
4055
|
+
this.indexedDBDump = modules.indexedDBDump;
|
|
4056
|
+
this.jsHeapSearch = modules.jsHeapSearch;
|
|
4057
|
+
this.tabWorkflow = modules.tabWorkflow;
|
|
4058
|
+
this.detailedData = modules.detailedData;
|
|
4059
|
+
this.jsdomHandlers = modules.jsdomHandlers;
|
|
4060
|
+
this.tabRegistry = modules.tabRegistry;
|
|
4061
|
+
}
|
|
4062
|
+
/** Get the shared TabRegistry for context enrichment. */
|
|
4063
|
+
getTabRegistry() {
|
|
4064
|
+
return this.tabRegistry;
|
|
4065
|
+
}
|
|
4066
|
+
/** Get or create camoufox page (Playwright Page). */
|
|
4067
|
+
async getCamoufoxPage() {
|
|
4068
|
+
if (!this.camoufoxManager) throw new Error("Camoufox browser not launched. Call browser_launch(driver=\"camoufox\") first.");
|
|
4069
|
+
if (!this.camoufoxPage) this.camoufoxPage = await this.camoufoxManager.newPage();
|
|
4070
|
+
return this.camoufoxPage;
|
|
4071
|
+
}
|
|
4072
|
+
async closeCamoufox() {
|
|
4073
|
+
try {
|
|
4074
|
+
await this.consoleMonitor.disable();
|
|
4075
|
+
} catch (error) {
|
|
4076
|
+
logger.warn(`Failed to reset console monitor before closing Camoufox: ${String(error)}`);
|
|
4077
|
+
}
|
|
4078
|
+
this.consoleMonitor.clearPlaywrightPage();
|
|
4079
|
+
if (this.camoufoxManager) {
|
|
4080
|
+
await this.camoufoxManager.close();
|
|
4081
|
+
this.camoufoxManager = null;
|
|
4082
|
+
this.camoufoxPage = null;
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
async handleBrowserLaunch(args) {
|
|
4086
|
+
if (argString(args, "driver", "chrome") === "camoufox") return this.handleCamoufoxLaunch(args);
|
|
4087
|
+
if (this.activeDriver === "camoufox" && this.camoufoxManager) await this.closeCamoufox();
|
|
4088
|
+
this.activeDriver = "chrome";
|
|
4089
|
+
return this.browserControl.handleBrowserLaunch(args);
|
|
4090
|
+
}
|
|
4091
|
+
async handleBrowserClose(args) {
|
|
4092
|
+
if (this.activeDriver === "camoufox" && this.camoufoxManager) {
|
|
4093
|
+
await this.closeCamoufox();
|
|
4094
|
+
await this.browserControl.handleBrowserClose(args);
|
|
4095
|
+
this.activeDriver = "chrome";
|
|
4096
|
+
return { content: [{
|
|
4097
|
+
type: "text",
|
|
4098
|
+
text: JSON.stringify({
|
|
4099
|
+
success: true,
|
|
4100
|
+
message: "Camoufox browser closed"
|
|
4101
|
+
}, null, 2)
|
|
4102
|
+
}] };
|
|
4103
|
+
}
|
|
4104
|
+
return this.browserControl.handleBrowserClose(args);
|
|
4105
|
+
}
|
|
4106
|
+
async handleBrowserStatus(args) {
|
|
4107
|
+
if (this.activeDriver === "camoufox") {
|
|
4108
|
+
const running = !!this.camoufoxManager?.getBrowser();
|
|
4109
|
+
return { content: [{
|
|
4110
|
+
type: "text",
|
|
4111
|
+
text: JSON.stringify({
|
|
4112
|
+
success: true,
|
|
4113
|
+
driver: "camoufox",
|
|
4114
|
+
running,
|
|
4115
|
+
hasActivePage: !!this.camoufoxPage
|
|
4116
|
+
}, null, 2)
|
|
4117
|
+
}] };
|
|
4118
|
+
}
|
|
4119
|
+
return this.browserControl.handleBrowserStatus(args);
|
|
4120
|
+
}
|
|
4121
|
+
async handleBrowserListTabs(args) {
|
|
4122
|
+
return this.browserControl.handleBrowserListTabs(args);
|
|
4123
|
+
}
|
|
4124
|
+
async handleBrowserListCdpTargets(args) {
|
|
4125
|
+
return this.targetControl.handleBrowserListCdpTargets(args);
|
|
4126
|
+
}
|
|
4127
|
+
async handleBrowserSelectTab(args) {
|
|
4128
|
+
return this.browserControl.handleBrowserSelectTab(args);
|
|
4129
|
+
}
|
|
4130
|
+
async handleBrowserAttachCdpTarget(args) {
|
|
4131
|
+
return this.targetControl.handleBrowserAttachCdpTarget(args);
|
|
4132
|
+
}
|
|
4133
|
+
async handleBrowserDetachCdpTarget(args) {
|
|
4134
|
+
return this.targetControl.handleBrowserDetachCdpTarget(args);
|
|
4135
|
+
}
|
|
4136
|
+
async handleBrowserEvaluateCdpTarget(args) {
|
|
4137
|
+
return this.targetEvaluation.handleBrowserEvaluateCdpTarget(args);
|
|
4138
|
+
}
|
|
4139
|
+
async handleBrowserAttach(args) {
|
|
4140
|
+
if (this.activeDriver === "camoufox" && this.camoufoxManager) await this.closeCamoufox();
|
|
4141
|
+
this.activeDriver = "chrome";
|
|
4142
|
+
return this.browserControl.handleBrowserAttach(args);
|
|
4143
|
+
}
|
|
4144
|
+
async handleCamoufoxServerDispatch(args) {
|
|
4145
|
+
switch (String(args["action"] ?? "")) {
|
|
4146
|
+
case "close": return this.camoufoxBrowser.handleCamoufoxServerClose(args);
|
|
4147
|
+
case "status": return this.camoufoxBrowser.handleCamoufoxServerStatus(args);
|
|
4148
|
+
default: return this.camoufoxBrowser.handleCamoufoxServerLaunch(args);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
async handleCamoufoxServerLaunch(args) {
|
|
4152
|
+
return this.camoufoxBrowser.handleCamoufoxServerLaunch(args);
|
|
4153
|
+
}
|
|
4154
|
+
async handleCamoufoxServerClose(args) {
|
|
4155
|
+
return this.camoufoxBrowser.handleCamoufoxServerClose(args);
|
|
4156
|
+
}
|
|
4157
|
+
async handleCamoufoxServerStatus(args) {
|
|
4158
|
+
return this.camoufoxBrowser.handleCamoufoxServerStatus(args);
|
|
4159
|
+
}
|
|
4160
|
+
async handlePageNavigate(args) {
|
|
4161
|
+
if (this.activeDriver === "camoufox") return this.handleCamoufoxNavigate(args);
|
|
4162
|
+
return this.pageNavigation.handlePageNavigate(args);
|
|
4163
|
+
}
|
|
4164
|
+
async handlePageReload(args) {
|
|
4165
|
+
return this.pageNavigation.handlePageReload(args);
|
|
4166
|
+
}
|
|
4167
|
+
async handlePageBack(args) {
|
|
4168
|
+
return this.pageNavigation.handlePageBack(args);
|
|
4169
|
+
}
|
|
4170
|
+
async handlePageForward(args) {
|
|
4171
|
+
return this.pageNavigation.handlePageForward(args);
|
|
4172
|
+
}
|
|
4173
|
+
async handlePageClick(args) {
|
|
4174
|
+
return this.pageInteraction.handlePageClick(args);
|
|
4175
|
+
}
|
|
4176
|
+
async handlePageType(args) {
|
|
4177
|
+
return this.pageInteraction.handlePageType(args);
|
|
4178
|
+
}
|
|
4179
|
+
async handlePageSelect(args) {
|
|
4180
|
+
return this.pageInteraction.handlePageSelect(args);
|
|
4181
|
+
}
|
|
4182
|
+
async handlePageHover(args) {
|
|
4183
|
+
return this.pageInteraction.handlePageHover(args);
|
|
4184
|
+
}
|
|
4185
|
+
async handlePageScroll(args) {
|
|
4186
|
+
return this.pageInteraction.handlePageScroll(args);
|
|
4187
|
+
}
|
|
4188
|
+
async handlePagePressKey(args) {
|
|
4189
|
+
return this.pageInteraction.handlePagePressKey(args);
|
|
4190
|
+
}
|
|
4191
|
+
async handlePageEvaluate(args) {
|
|
4192
|
+
return this.pageEvaluation.handlePageEvaluate(args);
|
|
4193
|
+
}
|
|
4194
|
+
async handlePageScreenshot(args) {
|
|
4195
|
+
return this.pageEvaluation.handlePageScreenshot(args);
|
|
4196
|
+
}
|
|
4197
|
+
async handlePageInjectScript(args) {
|
|
4198
|
+
return this.pageEvaluation.handlePageInjectScript(args);
|
|
4199
|
+
}
|
|
4200
|
+
async handlePageWaitForSelector(args) {
|
|
4201
|
+
return this.pageEvaluation.handlePageWaitForSelector(args);
|
|
4202
|
+
}
|
|
4203
|
+
async handlePageCookiesDispatch(args) {
|
|
4204
|
+
const action = String(args["action"] ?? "");
|
|
4205
|
+
switch (action) {
|
|
4206
|
+
case "get": return this.pageData.handlePageGetCookies(args);
|
|
4207
|
+
case "set": return this.pageData.handlePageSetCookies(args);
|
|
4208
|
+
case "clear": {
|
|
4209
|
+
const expectedCount = args["expectedCount"];
|
|
4210
|
+
if (typeof expectedCount !== "number" || expectedCount < 0) return {
|
|
4211
|
+
content: [{
|
|
4212
|
+
type: "text",
|
|
4213
|
+
text: "action=clear requires expectedCount (number). Call action=get first to obtain the current cookie count."
|
|
4214
|
+
}],
|
|
4215
|
+
isError: true
|
|
4216
|
+
};
|
|
4217
|
+
const current = await this.pageData.getPageCookieCount();
|
|
4218
|
+
if (current !== expectedCount) return {
|
|
4219
|
+
content: [{
|
|
4220
|
+
type: "text",
|
|
4221
|
+
text: `Cookie count mismatch: expected ${expectedCount} but found ${current}. Call action=get to refresh, then retry with the correct count.`
|
|
4222
|
+
}],
|
|
4223
|
+
isError: true
|
|
4224
|
+
};
|
|
4225
|
+
return this.pageData.handlePageClearCookies(args);
|
|
4226
|
+
}
|
|
4227
|
+
default: return {
|
|
4228
|
+
content: [{
|
|
4229
|
+
type: "text",
|
|
4230
|
+
text: `Invalid action: "${action}". Expected one of: get, set, clear`
|
|
4231
|
+
}],
|
|
4232
|
+
isError: true
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
async handlePageSetViewport(args) {
|
|
4237
|
+
return this.pageData.handlePageSetViewport(args);
|
|
4238
|
+
}
|
|
4239
|
+
async handlePageEmulateDevice(args) {
|
|
4240
|
+
return this.pageData.handlePageEmulateDevice(args);
|
|
4241
|
+
}
|
|
4242
|
+
async handlePageLocalStorageDispatch(args) {
|
|
4243
|
+
const action = String(args["action"] ?? "");
|
|
4244
|
+
switch (action) {
|
|
4245
|
+
case "get": return this.pageData.handlePageGetLocalStorage(args);
|
|
4246
|
+
case "set": return this.pageData.handlePageSetLocalStorage(args);
|
|
4247
|
+
default: return {
|
|
4248
|
+
content: [{
|
|
4249
|
+
type: "text",
|
|
4250
|
+
text: `Invalid action: "${action}". Expected one of: get, set`
|
|
4251
|
+
}],
|
|
4252
|
+
isError: true
|
|
4253
|
+
};
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
async handleConsoleMonitor(args) {
|
|
4257
|
+
return this.consoleHandlers.handleConsoleMonitor(args);
|
|
4258
|
+
}
|
|
4259
|
+
async handleConsoleGetLogs(args) {
|
|
4260
|
+
return this.consoleHandlers.handleConsoleGetLogs(args);
|
|
4261
|
+
}
|
|
4262
|
+
async handleConsoleExecute(args) {
|
|
4263
|
+
return this.consoleHandlers.handleConsoleExecute(args);
|
|
4264
|
+
}
|
|
4265
|
+
async handleGetAllScripts(args) {
|
|
4266
|
+
return this.scriptManagement.handleGetAllScripts(args);
|
|
4267
|
+
}
|
|
4268
|
+
async handleGetScriptSource(args) {
|
|
4269
|
+
return this.scriptManagement.handleGetScriptSource(args);
|
|
4270
|
+
}
|
|
4271
|
+
async handleCaptchaDetect(args) {
|
|
4272
|
+
return this.captchaHandlers.handleCaptchaDetect(args);
|
|
4273
|
+
}
|
|
4274
|
+
async handleCaptchaWait(args) {
|
|
4275
|
+
return this.captchaHandlers.handleCaptchaWait(args);
|
|
4276
|
+
}
|
|
4277
|
+
async handleCaptchaConfig(args) {
|
|
4278
|
+
return this.captchaHandlers.handleCaptchaConfig(args);
|
|
4279
|
+
}
|
|
4280
|
+
async handleStealthInject(args) {
|
|
4281
|
+
return this.stealthInjection.handleStealthInject(args);
|
|
4282
|
+
}
|
|
4283
|
+
async handleStealthSetUserAgent(args) {
|
|
4284
|
+
return this.stealthInjection.handleStealthSetUserAgent(args);
|
|
4285
|
+
}
|
|
4286
|
+
async handleStealthConfigureJitter(args) {
|
|
4287
|
+
return this.stealthInjection.handleStealthConfigureJitter(args);
|
|
4288
|
+
}
|
|
4289
|
+
async handleStealthGenerateFingerprint(args) {
|
|
4290
|
+
return this.stealthInjection.handleStealthGenerateFingerprint(args);
|
|
4291
|
+
}
|
|
4292
|
+
async handleStealthVerify(args) {
|
|
4293
|
+
return this.stealthInjection.handleStealthVerify(args);
|
|
4294
|
+
}
|
|
4295
|
+
async handleCamoufoxGeolocation(args) {
|
|
4296
|
+
return this.stealthInjection.handleCamoufoxGeolocation(args);
|
|
4297
|
+
}
|
|
4298
|
+
async handleFrameworkStateExtract(args) {
|
|
4299
|
+
return this.frameworkState.handleFrameworkStateExtract(args);
|
|
4300
|
+
}
|
|
4301
|
+
async handleIndexedDBDump(args) {
|
|
4302
|
+
return this.indexedDBDump.handleIndexedDBDump(args);
|
|
4303
|
+
}
|
|
4304
|
+
async handleJSHeapSearch(args) {
|
|
4305
|
+
return this.jsHeapSearch.handleJSHeapSearch(args);
|
|
4306
|
+
}
|
|
4307
|
+
async handleTabWorkflow(args) {
|
|
4308
|
+
return this.tabWorkflow.handleTabWorkflow(args);
|
|
4309
|
+
}
|
|
4310
|
+
async handleGetDetailedData(args) {
|
|
4311
|
+
return this.detailedData.handleGetDetailedData(args);
|
|
4312
|
+
}
|
|
4313
|
+
async handleCamoufoxLaunch(args) {
|
|
4314
|
+
return handleCamoufoxLaunchFlow({
|
|
4315
|
+
setCamoufoxManager: (manager) => {
|
|
4316
|
+
this.camoufoxManager = manager;
|
|
4317
|
+
},
|
|
4318
|
+
setActiveDriver: (driver) => {
|
|
4319
|
+
this.activeDriver = driver;
|
|
4320
|
+
},
|
|
4321
|
+
clearCamoufoxPage: () => {
|
|
4322
|
+
this.camoufoxPage = null;
|
|
4323
|
+
}
|
|
4324
|
+
}, args);
|
|
4325
|
+
}
|
|
4326
|
+
async handleCamoufoxNavigate(args) {
|
|
4327
|
+
return handleCamoufoxNavigateFlow({
|
|
4328
|
+
getCamoufoxPage: () => this.getCamoufoxPage(),
|
|
4329
|
+
setConsoleMonitorPage: (page) => {
|
|
4330
|
+
this.consoleMonitor.setPlaywrightPage(page);
|
|
4331
|
+
}
|
|
4332
|
+
}, args);
|
|
4333
|
+
}
|
|
4334
|
+
async handleHumanMouse(args) {
|
|
4335
|
+
return handleHumanMouse(args, this.collector);
|
|
4336
|
+
}
|
|
4337
|
+
async handleHumanScroll(args) {
|
|
4338
|
+
return handleHumanScroll(args, this.collector);
|
|
4339
|
+
}
|
|
4340
|
+
async handleHumanTyping(args) {
|
|
4341
|
+
return handleHumanTyping(args, this.collector);
|
|
4342
|
+
}
|
|
4343
|
+
async handleCaptchaVisionSolve(args) {
|
|
4344
|
+
return handleCaptchaVisionSolve(args, this.collector);
|
|
4345
|
+
}
|
|
4346
|
+
async handleWidgetChallengeSolve(args) {
|
|
4347
|
+
return handleWidgetChallengeSolve(args, this.collector);
|
|
4348
|
+
}
|
|
4349
|
+
async handleCaptchaSolverCapabilities() {
|
|
4350
|
+
return handleCaptchaSolverCapabilities(this.collector);
|
|
4351
|
+
}
|
|
4352
|
+
async handleJsdomParse(args) {
|
|
4353
|
+
return this.jsdomHandlers.handleJsdomParse(args);
|
|
4354
|
+
}
|
|
4355
|
+
async handleJsdomQuery(args) {
|
|
4356
|
+
return this.jsdomHandlers.handleJsdomQuery(args);
|
|
4357
|
+
}
|
|
4358
|
+
async handleJsdomExecute(args) {
|
|
4359
|
+
return this.jsdomHandlers.handleJsdomExecute(args);
|
|
4360
|
+
}
|
|
4361
|
+
async handleJsdomSerialize(args) {
|
|
4362
|
+
return this.jsdomHandlers.handleJsdomSerialize(args);
|
|
4363
|
+
}
|
|
4364
|
+
async handleJsdomCookies(args) {
|
|
4365
|
+
return this.jsdomHandlers.handleJsdomCookies(args);
|
|
4366
|
+
}
|
|
4367
|
+
};
|
|
4368
|
+
//#endregion
|
|
4369
|
+
export { BrowserToolHandlers };
|