@flrande/browserctl 0.5.0 → 0.6.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/dist/client.d.ts +34 -0
- package/dist/client.js +138 -0
- package/dist/commandRegistry.d.ts +16 -0
- package/dist/commandRegistry.js +21 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/runCli.d.ts +5 -0
- package/dist/runCli.js +170 -0
- package/package.json +32 -57
- package/INSTALL-CN.md +0 -92
- package/INSTALL.md +0 -92
- package/LICENSE +0 -21
- package/README-CN.md +0 -69
- package/README.md +0 -69
- package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
- package/apps/browserctl/src/commands/act.test.ts +0 -71
- package/apps/browserctl/src/commands/act.ts +0 -64
- package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
- package/apps/browserctl/src/commands/common.test.ts +0 -87
- package/apps/browserctl/src/commands/common.ts +0 -191
- package/apps/browserctl/src/commands/console-list.test.ts +0 -102
- package/apps/browserctl/src/commands/console-list.ts +0 -108
- package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
- package/apps/browserctl/src/commands/cookie-get.ts +0 -18
- package/apps/browserctl/src/commands/cookie-set.ts +0 -22
- package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
- package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
- package/apps/browserctl/src/commands/dom-query.ts +0 -18
- package/apps/browserctl/src/commands/download-trigger.ts +0 -22
- package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
- package/apps/browserctl/src/commands/download-wait.ts +0 -27
- package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
- package/apps/browserctl/src/commands/frame-list.ts +0 -16
- package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
- package/apps/browserctl/src/commands/har-export.test.ts +0 -112
- package/apps/browserctl/src/commands/har-export.ts +0 -120
- package/apps/browserctl/src/commands/memory-delete.ts +0 -20
- package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
- package/apps/browserctl/src/commands/memory-list.ts +0 -90
- package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
- package/apps/browserctl/src/commands/memory-purge.ts +0 -16
- package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
- package/apps/browserctl/src/commands/memory-status.ts +0 -16
- package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
- package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
- package/apps/browserctl/src/commands/network-list.test.ts +0 -110
- package/apps/browserctl/src/commands/network-list.ts +0 -112
- package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
- package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
- package/apps/browserctl/src/commands/profile-list.ts +0 -16
- package/apps/browserctl/src/commands/profile-use.ts +0 -18
- package/apps/browserctl/src/commands/response-body.ts +0 -24
- package/apps/browserctl/src/commands/screenshot.ts +0 -16
- package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
- package/apps/browserctl/src/commands/session-drop.ts +0 -16
- package/apps/browserctl/src/commands/session-list.test.ts +0 -81
- package/apps/browserctl/src/commands/session-list.ts +0 -70
- package/apps/browserctl/src/commands/snapshot.ts +0 -16
- package/apps/browserctl/src/commands/status.ts +0 -10
- package/apps/browserctl/src/commands/storage-get.ts +0 -20
- package/apps/browserctl/src/commands/storage-set.ts +0 -22
- package/apps/browserctl/src/commands/tab-close.ts +0 -20
- package/apps/browserctl/src/commands/tab-focus.ts +0 -20
- package/apps/browserctl/src/commands/tab-open.ts +0 -19
- package/apps/browserctl/src/commands/tabs.ts +0 -13
- package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
- package/apps/browserctl/src/commands/trace-get.ts +0 -62
- package/apps/browserctl/src/commands/upload-arm.ts +0 -26
- package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-element.ts +0 -76
- package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
- package/apps/browserctl/src/commands/wait-text.ts +0 -93
- package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-url.ts +0 -76
- package/apps/browserctl/src/daemon-client.test.ts +0 -253
- package/apps/browserctl/src/daemon-client.ts +0 -632
- package/apps/browserctl/src/e2e.test.ts +0 -103
- package/apps/browserctl/src/main.dispatch.test.ts +0 -461
- package/apps/browserctl/src/main.test.ts +0 -334
- package/apps/browserctl/src/main.ts +0 -957
- package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
- package/apps/browserctl/src/test-port.ts +0 -26
- package/apps/browserd/src/bootstrap.ts +0 -432
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
- package/apps/browserd/src/container.ts +0 -3088
- package/apps/browserd/src/main.test.ts +0 -1436
- package/apps/browserd/src/main.ts +0 -7
- package/apps/browserd/src/test-port.ts +0 -26
- package/apps/browserd/src/tool-matrix.test.ts +0 -887
- package/bin/browserctl.cjs +0 -21
- package/bin/browserd.cjs +0 -21
- package/extensions/chrome-relay/README-CN.md +0 -39
- package/extensions/chrome-relay/README.md +0 -39
- package/extensions/chrome-relay/background.js +0 -1687
- package/extensions/chrome-relay/manifest.json +0 -15
- package/extensions/chrome-relay/popup.html +0 -369
- package/extensions/chrome-relay/popup.js +0 -972
- package/packages/core/src/bootstrap.test.ts +0 -10
- package/packages/core/src/driver-registry.test.ts +0 -45
- package/packages/core/src/driver-registry.ts +0 -22
- package/packages/core/src/driver.ts +0 -47
- package/packages/core/src/index.ts +0 -6
- package/packages/core/src/navigation-memory.test.ts +0 -259
- package/packages/core/src/navigation-memory.ts +0 -360
- package/packages/core/src/ref-cache.test.ts +0 -61
- package/packages/core/src/ref-cache.ts +0 -28
- package/packages/core/src/session-store.test.ts +0 -82
- package/packages/core/src/session-store.ts +0 -138
- package/packages/core/src/types.ts +0 -9
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
- package/packages/driver-chrome-relay/src/index.ts +0 -26
- package/packages/driver-managed/src/index.ts +0 -22
- package/packages/driver-managed/src/managed-driver.test.ts +0 -183
- package/packages/driver-managed/src/managed-driver.ts +0 -341
- package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
- package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
- package/packages/driver-remote-cdp/src/index.ts +0 -19
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
- package/packages/protocol/src/envelope.test.ts +0 -25
- package/packages/protocol/src/envelope.ts +0 -31
- package/packages/protocol/src/errors.test.ts +0 -17
- package/packages/protocol/src/errors.ts +0 -11
- package/packages/protocol/src/index.ts +0 -3
- package/packages/protocol/src/tools.ts +0 -3
- package/packages/transport-mcp-stdio/src/index.ts +0 -3
- package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
- package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
- package/packages/transport-mcp-stdio/src/server.ts +0 -183
- package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
|
@@ -1,1687 +0,0 @@
|
|
|
1
|
-
const CONFIG_KEY = "browserctlRelayConfig";
|
|
2
|
-
const DEFAULT_BRIDGE_URL = "ws://127.0.0.1:9223/bridge";
|
|
3
|
-
const RECONNECT_DELAY_MS = 1500;
|
|
4
|
-
const DEBUGGER_PROTOCOL_VERSION = "1.3";
|
|
5
|
-
const TELEMETRY_BODY_MAX_CHARS = 262144;
|
|
6
|
-
|
|
7
|
-
let socket = null;
|
|
8
|
-
let reconnectTimer = null;
|
|
9
|
-
let connectInFlight = false;
|
|
10
|
-
let telemetryRequestSequence = 1;
|
|
11
|
-
let runtimeState = {
|
|
12
|
-
connected: false,
|
|
13
|
-
bridgeUrl: DEFAULT_BRIDGE_URL,
|
|
14
|
-
token: "",
|
|
15
|
-
lastError: "",
|
|
16
|
-
lastConnectedAt: ""
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const attachedDebuggerTabs = new Set();
|
|
20
|
-
const networkRequestMetaByTab = new Map();
|
|
21
|
-
|
|
22
|
-
function normalizeString(value) {
|
|
23
|
-
if (typeof value !== "string") {
|
|
24
|
-
return "";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return value.trim();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function normalizeBridgeUrl(value) {
|
|
31
|
-
const rawValue = normalizeString(value);
|
|
32
|
-
if (rawValue.length === 0) {
|
|
33
|
-
return DEFAULT_BRIDGE_URL;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const parsed = new URL(rawValue);
|
|
38
|
-
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
39
|
-
return DEFAULT_BRIDGE_URL;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return parsed.toString();
|
|
43
|
-
} catch {
|
|
44
|
-
return DEFAULT_BRIDGE_URL;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function toErrorMessage(error) {
|
|
49
|
-
if (error instanceof Error) {
|
|
50
|
-
return error.message;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return String(error);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function buildSocketUrl(config) {
|
|
57
|
-
return normalizeBridgeUrl(config.bridgeUrl);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function readConfig() {
|
|
61
|
-
const stored = await chrome.storage.local.get(CONFIG_KEY);
|
|
62
|
-
const rawConfig = stored?.[CONFIG_KEY];
|
|
63
|
-
const config = {
|
|
64
|
-
bridgeUrl: DEFAULT_BRIDGE_URL,
|
|
65
|
-
token: ""
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
if (rawConfig !== null && typeof rawConfig === "object") {
|
|
69
|
-
config.bridgeUrl = normalizeBridgeUrl(rawConfig.bridgeUrl);
|
|
70
|
-
config.token = normalizeString(rawConfig.token);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
runtimeState.bridgeUrl = config.bridgeUrl;
|
|
74
|
-
runtimeState.token = config.token;
|
|
75
|
-
return config;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function writeConfig(nextConfig) {
|
|
79
|
-
const normalized = {
|
|
80
|
-
bridgeUrl: normalizeBridgeUrl(nextConfig.bridgeUrl),
|
|
81
|
-
token: normalizeString(nextConfig.token)
|
|
82
|
-
};
|
|
83
|
-
await chrome.storage.local.set({
|
|
84
|
-
[CONFIG_KEY]: normalized
|
|
85
|
-
});
|
|
86
|
-
runtimeState.bridgeUrl = normalized.bridgeUrl;
|
|
87
|
-
runtimeState.token = normalized.token;
|
|
88
|
-
return normalized;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function setDisconnectedState(reason) {
|
|
92
|
-
runtimeState.connected = false;
|
|
93
|
-
if (reason.length > 0) {
|
|
94
|
-
runtimeState.lastError = reason;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function clearReconnectTimer() {
|
|
99
|
-
if (reconnectTimer === null) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
clearTimeout(reconnectTimer);
|
|
104
|
-
reconnectTimer = null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function scheduleReconnect() {
|
|
108
|
-
if (reconnectTimer !== null) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
reconnectTimer = setTimeout(() => {
|
|
113
|
-
reconnectTimer = null;
|
|
114
|
-
void connectToBridge(false);
|
|
115
|
-
}, RECONNECT_DELAY_MS);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function sendSocketEnvelope(payload) {
|
|
119
|
-
if (socket === null || socket.readyState !== WebSocket.OPEN) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
socket.send(JSON.stringify(payload));
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function sendBridgeEvent(event) {
|
|
128
|
-
sendSocketEnvelope({
|
|
129
|
-
type: "event",
|
|
130
|
-
event
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function sendHello() {
|
|
135
|
-
sendSocketEnvelope({
|
|
136
|
-
type: "hello",
|
|
137
|
-
role: "extension",
|
|
138
|
-
extensionId: chrome.runtime.id,
|
|
139
|
-
version: chrome.runtime.getManifest().version,
|
|
140
|
-
token: runtimeState.token
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function executeInTab(tabId, func, args = []) {
|
|
145
|
-
const results = await chrome.scripting.executeScript({
|
|
146
|
-
target: { tabId },
|
|
147
|
-
func,
|
|
148
|
-
args
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (!Array.isArray(results) || results.length === 0) {
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return results[0]?.result;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function toTabSummary(tab) {
|
|
159
|
-
return {
|
|
160
|
-
tabId: typeof tab.id === "number" ? tab.id : undefined,
|
|
161
|
-
windowId: typeof tab.windowId === "number" ? tab.windowId : undefined,
|
|
162
|
-
active: tab.active === true,
|
|
163
|
-
url: typeof tab.url === "string" ? tab.url : "",
|
|
164
|
-
title: typeof tab.title === "string" ? tab.title : ""
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function requireNonEmptyString(value, fieldName) {
|
|
169
|
-
const normalized = normalizeString(value);
|
|
170
|
-
if (normalized.length === 0) {
|
|
171
|
-
throw new Error(`${fieldName} must be a non-empty string.`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return normalized;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function requireTabId(value, fieldName = "tabId") {
|
|
178
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
179
|
-
throw new Error(`${fieldName} must be a number.`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return value;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getNetworkRequestMap(tabId) {
|
|
186
|
-
const existing = networkRequestMetaByTab.get(tabId);
|
|
187
|
-
if (existing !== undefined) {
|
|
188
|
-
return existing;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const created = new Map();
|
|
192
|
-
networkRequestMetaByTab.set(tabId, created);
|
|
193
|
-
return created;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function clearNetworkRequestState(tabId) {
|
|
197
|
-
networkRequestMetaByTab.delete(tabId);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function getOrCreateNetworkRequestMeta(tabId, rawRequestId) {
|
|
201
|
-
const requestMap = getNetworkRequestMap(tabId);
|
|
202
|
-
const existing = requestMap.get(rawRequestId);
|
|
203
|
-
if (existing !== undefined) {
|
|
204
|
-
return existing;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const created = {
|
|
208
|
-
requestId: `request:extension:${tabId}:${telemetryRequestSequence}`,
|
|
209
|
-
rawRequestId,
|
|
210
|
-
url: "",
|
|
211
|
-
method: undefined,
|
|
212
|
-
resourceType: undefined,
|
|
213
|
-
status: undefined
|
|
214
|
-
};
|
|
215
|
-
telemetryRequestSequence += 1;
|
|
216
|
-
requestMap.set(rawRequestId, created);
|
|
217
|
-
return created;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function readRemoteObjectValue(value) {
|
|
221
|
-
if (value === null || typeof value !== "object") {
|
|
222
|
-
return "";
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if ("value" in value && value.value !== undefined) {
|
|
226
|
-
try {
|
|
227
|
-
if (typeof value.value === "string") {
|
|
228
|
-
return value.value;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return JSON.stringify(value.value);
|
|
232
|
-
} catch {
|
|
233
|
-
return String(value.value);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if ("unserializableValue" in value && typeof value.unserializableValue === "string") {
|
|
238
|
-
return value.unserializableValue;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if ("description" in value && typeof value.description === "string") {
|
|
242
|
-
return value.description;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return "";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function toConsoleEntry(params) {
|
|
249
|
-
const args = Array.isArray(params?.args) ? params.args : [];
|
|
250
|
-
const text = args.map((item) => readRemoteObjectValue(item)).join(" ");
|
|
251
|
-
const stackFrames = Array.isArray(params?.stackTrace?.callFrames)
|
|
252
|
-
? params.stackTrace.callFrames
|
|
253
|
-
: [];
|
|
254
|
-
const firstFrame = stackFrames[0];
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
type: normalizeString(params?.type) || "log",
|
|
258
|
-
text,
|
|
259
|
-
...(firstFrame !== undefined
|
|
260
|
-
? {
|
|
261
|
-
location: {
|
|
262
|
-
...(normalizeString(firstFrame.url).length > 0 ? { url: firstFrame.url } : {}),
|
|
263
|
-
...(typeof firstFrame.lineNumber === "number"
|
|
264
|
-
? { lineNumber: firstFrame.lineNumber }
|
|
265
|
-
: {}),
|
|
266
|
-
...(typeof firstFrame.columnNumber === "number"
|
|
267
|
-
? { columnNumber: firstFrame.columnNumber }
|
|
268
|
-
: {})
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
: {})
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function normalizeTelemetryBody(body, base64Encoded) {
|
|
276
|
-
if (typeof body !== "string") {
|
|
277
|
-
return {
|
|
278
|
-
body: "",
|
|
279
|
-
encoding: "utf8"
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const sliced = body.slice(0, TELEMETRY_BODY_MAX_CHARS);
|
|
284
|
-
return {
|
|
285
|
-
body: sliced,
|
|
286
|
-
encoding: base64Encoded === true ? "base64" : "utf8"
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function attachDebuggerToTab(tabId) {
|
|
291
|
-
if (attachedDebuggerTabs.has(tabId)) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
let attachSucceeded = false;
|
|
296
|
-
try {
|
|
297
|
-
await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
|
|
298
|
-
attachSucceeded = true;
|
|
299
|
-
} catch (error) {
|
|
300
|
-
const errorMessage = toErrorMessage(error).toLowerCase();
|
|
301
|
-
if (errorMessage.includes("already attached")) {
|
|
302
|
-
attachSucceeded = true;
|
|
303
|
-
} else if (errorMessage.includes("another debugger")) {
|
|
304
|
-
throw new Error(`Tab ${tabId} is already attached by another debugger.`);
|
|
305
|
-
} else {
|
|
306
|
-
throw error;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (!attachSucceeded) {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
attachedDebuggerTabs.add(tabId);
|
|
315
|
-
try {
|
|
316
|
-
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
|
|
317
|
-
} catch {
|
|
318
|
-
// Ignore optional domain initialization failures.
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
await chrome.debugger.sendCommand({ tabId }, "Network.enable");
|
|
323
|
-
} catch {
|
|
324
|
-
// Ignore optional domain initialization failures.
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
await chrome.debugger.sendCommand({ tabId }, "DOM.enable");
|
|
329
|
-
} catch {
|
|
330
|
-
// Ignore optional domain initialization failures.
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async function detachDebuggerFromTab(tabId) {
|
|
335
|
-
if (!attachedDebuggerTabs.has(tabId)) {
|
|
336
|
-
clearNetworkRequestState(tabId);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
attachedDebuggerTabs.delete(tabId);
|
|
341
|
-
clearNetworkRequestState(tabId);
|
|
342
|
-
try {
|
|
343
|
-
await chrome.debugger.detach({ tabId });
|
|
344
|
-
} catch {
|
|
345
|
-
// Ignore detach errors for already closed tabs.
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function ensureDebuggerAttached(tabId) {
|
|
350
|
-
try {
|
|
351
|
-
await attachDebuggerToTab(tabId);
|
|
352
|
-
return true;
|
|
353
|
-
} catch (error) {
|
|
354
|
-
runtimeState.lastError = `Debugger attach failed for tab ${tabId}: ${toErrorMessage(error)}`;
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function emitNetworkResponseEvent(tabId, rawRequestId) {
|
|
360
|
-
const requestMap = networkRequestMetaByTab.get(tabId);
|
|
361
|
-
const requestMeta = requestMap?.get(rawRequestId);
|
|
362
|
-
if (requestMeta === undefined) {
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
let responseBodyPayload = {
|
|
367
|
-
body: "",
|
|
368
|
-
encoding: "utf8"
|
|
369
|
-
};
|
|
370
|
-
try {
|
|
371
|
-
const responseBody = await chrome.debugger.sendCommand(
|
|
372
|
-
{ tabId },
|
|
373
|
-
"Network.getResponseBody",
|
|
374
|
-
{ requestId: rawRequestId }
|
|
375
|
-
);
|
|
376
|
-
responseBodyPayload = normalizeTelemetryBody(
|
|
377
|
-
responseBody?.body,
|
|
378
|
-
responseBody?.base64Encoded === true
|
|
379
|
-
);
|
|
380
|
-
} catch {
|
|
381
|
-
// Some resource types do not expose bodies; keep empty body payload.
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
sendBridgeEvent({
|
|
385
|
-
kind: "response",
|
|
386
|
-
tabId,
|
|
387
|
-
response: {
|
|
388
|
-
requestId: requestMeta.requestId,
|
|
389
|
-
url: requestMeta.url,
|
|
390
|
-
...(typeof requestMeta.status === "number" ? { status: requestMeta.status } : {}),
|
|
391
|
-
...(typeof requestMeta.method === "string" ? { method: requestMeta.method } : {}),
|
|
392
|
-
...(typeof requestMeta.resourceType === "string"
|
|
393
|
-
? { resourceType: requestMeta.resourceType }
|
|
394
|
-
: {}),
|
|
395
|
-
...responseBodyPayload
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
requestMap.delete(rawRequestId);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function handleDebuggerEvent(source, method, params) {
|
|
403
|
-
if (typeof source?.tabId !== "number") {
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const tabId = source.tabId;
|
|
408
|
-
if (method === "Runtime.consoleAPICalled") {
|
|
409
|
-
sendBridgeEvent({
|
|
410
|
-
kind: "console",
|
|
411
|
-
tabId,
|
|
412
|
-
entry: toConsoleEntry(params)
|
|
413
|
-
});
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (method === "Network.requestWillBeSent") {
|
|
418
|
-
const rawRequestId = normalizeString(params?.requestId);
|
|
419
|
-
if (rawRequestId.length === 0) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const requestMeta = getOrCreateNetworkRequestMeta(tabId, rawRequestId);
|
|
424
|
-
requestMeta.url = normalizeString(params?.request?.url);
|
|
425
|
-
const methodValue = normalizeString(params?.request?.method);
|
|
426
|
-
if (methodValue.length > 0) {
|
|
427
|
-
requestMeta.method = methodValue;
|
|
428
|
-
}
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (method === "Network.responseReceived") {
|
|
433
|
-
const rawRequestId = normalizeString(params?.requestId);
|
|
434
|
-
if (rawRequestId.length === 0) {
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const requestMeta = getOrCreateNetworkRequestMeta(tabId, rawRequestId);
|
|
439
|
-
const responseUrl = normalizeString(params?.response?.url);
|
|
440
|
-
if (responseUrl.length > 0) {
|
|
441
|
-
requestMeta.url = responseUrl;
|
|
442
|
-
}
|
|
443
|
-
if (typeof params?.response?.status === "number") {
|
|
444
|
-
requestMeta.status = params.response.status;
|
|
445
|
-
}
|
|
446
|
-
const resourceType = normalizeString(params?.type);
|
|
447
|
-
if (resourceType.length > 0) {
|
|
448
|
-
requestMeta.resourceType = resourceType;
|
|
449
|
-
}
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (method === "Network.loadingFinished") {
|
|
454
|
-
const rawRequestId = normalizeString(params?.requestId);
|
|
455
|
-
if (rawRequestId.length === 0) {
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
void emitNetworkResponseEvent(tabId, rawRequestId);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (method === "Network.loadingFailed") {
|
|
464
|
-
const rawRequestId = normalizeString(params?.requestId);
|
|
465
|
-
if (rawRequestId.length === 0) {
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const requestMap = networkRequestMetaByTab.get(tabId);
|
|
470
|
-
requestMap?.delete(rawRequestId);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function registerDebuggerTelemetryHandlers() {
|
|
475
|
-
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
476
|
-
handleDebuggerEvent(source, method, params);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
chrome.debugger.onDetach.addListener((source) => {
|
|
480
|
-
if (typeof source?.tabId !== "number") {
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
attachedDebuggerTabs.delete(source.tabId);
|
|
485
|
-
clearNetworkRequestState(source.tabId);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
489
|
-
void detachDebuggerFromTab(tabId);
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async function handleTabOpen(params) {
|
|
494
|
-
const url = requireNonEmptyString(params?.url, "url");
|
|
495
|
-
const tab = await chrome.tabs.create({
|
|
496
|
-
url,
|
|
497
|
-
active: true
|
|
498
|
-
});
|
|
499
|
-
if (typeof tab.id === "number") {
|
|
500
|
-
await ensureDebuggerAttached(tab.id);
|
|
501
|
-
}
|
|
502
|
-
return toTabSummary(tab);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
async function handleTabGoto(params) {
|
|
506
|
-
const tabId = requireTabId(params?.tabId);
|
|
507
|
-
const url = requireNonEmptyString(params?.url, "url");
|
|
508
|
-
await ensureDebuggerAttached(tabId);
|
|
509
|
-
const tab = await chrome.tabs.update(tabId, {
|
|
510
|
-
url,
|
|
511
|
-
active: true
|
|
512
|
-
});
|
|
513
|
-
return toTabSummary(tab);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async function handleTabFocus(params) {
|
|
517
|
-
const tabId = requireTabId(params?.tabId);
|
|
518
|
-
await ensureDebuggerAttached(tabId);
|
|
519
|
-
const tab = await chrome.tabs.get(tabId);
|
|
520
|
-
if (typeof tab.windowId === "number") {
|
|
521
|
-
await chrome.windows.update(tab.windowId, { focused: true });
|
|
522
|
-
}
|
|
523
|
-
const updated = await chrome.tabs.update(tabId, { active: true });
|
|
524
|
-
return toTabSummary(updated);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async function handleTabClose(params) {
|
|
528
|
-
const tabId = requireTabId(params?.tabId);
|
|
529
|
-
await detachDebuggerFromTab(tabId);
|
|
530
|
-
await chrome.tabs.remove(tabId);
|
|
531
|
-
return {
|
|
532
|
-
tabId,
|
|
533
|
-
closed: true
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async function handleTabInfo(params) {
|
|
538
|
-
const tabId = requireTabId(params?.tabId);
|
|
539
|
-
await ensureDebuggerAttached(tabId);
|
|
540
|
-
const tab = await chrome.tabs.get(tabId);
|
|
541
|
-
return toTabSummary(tab);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
async function handleTabSnapshot(params) {
|
|
545
|
-
const tabId = requireTabId(params?.tabId);
|
|
546
|
-
await ensureDebuggerAttached(tabId);
|
|
547
|
-
const result = await executeInTab(
|
|
548
|
-
tabId,
|
|
549
|
-
() => ({
|
|
550
|
-
url: window.location.href,
|
|
551
|
-
title: document.title ?? "",
|
|
552
|
-
html: document.documentElement ? document.documentElement.outerHTML : ""
|
|
553
|
-
}),
|
|
554
|
-
[]
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
if (result !== null && typeof result === "object") {
|
|
558
|
-
return {
|
|
559
|
-
tabId,
|
|
560
|
-
url: typeof result.url === "string" ? result.url : "",
|
|
561
|
-
title: typeof result.title === "string" ? result.title : "",
|
|
562
|
-
html: typeof result.html === "string" ? result.html : ""
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const tab = await chrome.tabs.get(tabId);
|
|
567
|
-
return {
|
|
568
|
-
tabId,
|
|
569
|
-
url: typeof tab.url === "string" ? tab.url : "",
|
|
570
|
-
title: typeof tab.title === "string" ? tab.title : "",
|
|
571
|
-
html: ""
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function parseCapturedImageDataUrl(dataUrl) {
|
|
576
|
-
const normalized = normalizeString(dataUrl);
|
|
577
|
-
const match = /^data:([^;,]+);base64,(.+)$/i.exec(normalized);
|
|
578
|
-
if (match === null) {
|
|
579
|
-
throw new Error("tabs.captureVisibleTab returned unsupported image payload.");
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const mimeType = match[1] || "image/png";
|
|
583
|
-
const imageBase64 = match[2] || "";
|
|
584
|
-
if (imageBase64.length === 0) {
|
|
585
|
-
throw new Error("tabs.captureVisibleTab returned empty image payload.");
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
mimeType,
|
|
590
|
-
imageBase64
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async function cropCapturedImageDataUrl(dataUrl, bounds) {
|
|
595
|
-
const fallback = parseCapturedImageDataUrl(dataUrl);
|
|
596
|
-
if (
|
|
597
|
-
typeof OffscreenCanvas !== "function" ||
|
|
598
|
-
typeof createImageBitmap !== "function" ||
|
|
599
|
-
typeof bounds !== "object" ||
|
|
600
|
-
bounds === null
|
|
601
|
-
) {
|
|
602
|
-
return fallback;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const x = typeof bounds.x === "number" && Number.isFinite(bounds.x) ? bounds.x : 0;
|
|
606
|
-
const y = typeof bounds.y === "number" && Number.isFinite(bounds.y) ? bounds.y : 0;
|
|
607
|
-
const width = typeof bounds.width === "number" && Number.isFinite(bounds.width) ? bounds.width : 0;
|
|
608
|
-
const height = typeof bounds.height === "number" && Number.isFinite(bounds.height) ? bounds.height : 0;
|
|
609
|
-
if (width <= 0 || height <= 0) {
|
|
610
|
-
return fallback;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
try {
|
|
614
|
-
const response = await fetch(dataUrl);
|
|
615
|
-
const imageBlob = await response.blob();
|
|
616
|
-
const bitmap = await createImageBitmap(imageBlob);
|
|
617
|
-
const sx = Math.max(0, Math.min(bitmap.width - 1, Math.floor(x)));
|
|
618
|
-
const sy = Math.max(0, Math.min(bitmap.height - 1, Math.floor(y)));
|
|
619
|
-
const sw = Math.max(1, Math.min(bitmap.width - sx, Math.ceil(width)));
|
|
620
|
-
const sh = Math.max(1, Math.min(bitmap.height - sy, Math.ceil(height)));
|
|
621
|
-
const canvas = new OffscreenCanvas(sw, sh);
|
|
622
|
-
const context = canvas.getContext("2d");
|
|
623
|
-
if (context === null) {
|
|
624
|
-
return fallback;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
context.drawImage(bitmap, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
628
|
-
const croppedBlob = await canvas.convertToBlob({
|
|
629
|
-
type: "image/png"
|
|
630
|
-
});
|
|
631
|
-
const croppedBuffer = await croppedBlob.arrayBuffer();
|
|
632
|
-
const bytes = new Uint8Array(croppedBuffer);
|
|
633
|
-
let binary = "";
|
|
634
|
-
for (let index = 0; index < bytes.length; index += 1) {
|
|
635
|
-
binary += String.fromCharCode(bytes[index]);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
return {
|
|
639
|
-
mimeType: "image/png",
|
|
640
|
-
imageBase64: btoa(binary),
|
|
641
|
-
width: sw,
|
|
642
|
-
height: sh
|
|
643
|
-
};
|
|
644
|
-
} catch {
|
|
645
|
-
return fallback;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async function handleTabScreenshot(params) {
|
|
650
|
-
const tabId = requireTabId(params?.tabId);
|
|
651
|
-
await ensureDebuggerAttached(tabId);
|
|
652
|
-
const tab = await chrome.tabs.get(tabId);
|
|
653
|
-
if (typeof tab.windowId !== "number") {
|
|
654
|
-
throw new Error(`Unable to resolve window for tab ${tabId}.`);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
await chrome.windows.update(tab.windowId, { focused: true });
|
|
658
|
-
await chrome.tabs.update(tabId, { active: true });
|
|
659
|
-
|
|
660
|
-
const rawDataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
|
|
661
|
-
format: "png"
|
|
662
|
-
});
|
|
663
|
-
const parsedImage = parseCapturedImageDataUrl(rawDataUrl);
|
|
664
|
-
|
|
665
|
-
const dimensions = await executeInTab(
|
|
666
|
-
tabId,
|
|
667
|
-
() => ({
|
|
668
|
-
width:
|
|
669
|
-
typeof window.innerWidth === "number" && typeof window.devicePixelRatio === "number"
|
|
670
|
-
? Math.max(1, Math.round(window.innerWidth * window.devicePixelRatio))
|
|
671
|
-
: undefined,
|
|
672
|
-
height:
|
|
673
|
-
typeof window.innerHeight === "number" && typeof window.devicePixelRatio === "number"
|
|
674
|
-
? Math.max(1, Math.round(window.innerHeight * window.devicePixelRatio))
|
|
675
|
-
: undefined
|
|
676
|
-
}),
|
|
677
|
-
[]
|
|
678
|
-
);
|
|
679
|
-
|
|
680
|
-
return {
|
|
681
|
-
tabId,
|
|
682
|
-
mimeType: parsedImage.mimeType,
|
|
683
|
-
encoding: "base64",
|
|
684
|
-
imageBase64: parsedImage.imageBase64,
|
|
685
|
-
...(typeof dimensions?.width === "number" ? { width: dimensions.width } : {}),
|
|
686
|
-
...(typeof dimensions?.height === "number" ? { height: dimensions.height } : {})
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
async function handleTabDomQuery(params) {
|
|
691
|
-
const tabId = requireTabId(params?.tabId);
|
|
692
|
-
const selector = requireNonEmptyString(params?.selector, "selector");
|
|
693
|
-
await ensureDebuggerAttached(tabId);
|
|
694
|
-
const result = await executeInTab(
|
|
695
|
-
tabId,
|
|
696
|
-
(selectorValue) => {
|
|
697
|
-
let element = null;
|
|
698
|
-
try {
|
|
699
|
-
element = document.querySelector(selectorValue);
|
|
700
|
-
} catch {
|
|
701
|
-
return {
|
|
702
|
-
selector: selectorValue,
|
|
703
|
-
found: false
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (element === null) {
|
|
708
|
-
return {
|
|
709
|
-
selector: selectorValue,
|
|
710
|
-
found: false
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const attributes = {};
|
|
715
|
-
for (const attribute of Array.from(element.attributes)) {
|
|
716
|
-
attributes[attribute.name] = attribute.value;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
return {
|
|
720
|
-
selector: selectorValue,
|
|
721
|
-
found: true,
|
|
722
|
-
node: {
|
|
723
|
-
tagName: element.tagName.toLowerCase(),
|
|
724
|
-
id: element.id || undefined,
|
|
725
|
-
className: element.className || undefined,
|
|
726
|
-
text: (element.textContent || "").trim().slice(0, 300),
|
|
727
|
-
attributes
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
},
|
|
731
|
-
[selector]
|
|
732
|
-
);
|
|
733
|
-
|
|
734
|
-
if (result !== null && typeof result === "object") {
|
|
735
|
-
return result;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
return {
|
|
739
|
-
selector,
|
|
740
|
-
found: false
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
async function handleTabDomQueryAll(params) {
|
|
745
|
-
const tabId = requireTabId(params?.tabId);
|
|
746
|
-
const selector = requireNonEmptyString(params?.selector, "selector");
|
|
747
|
-
await ensureDebuggerAttached(tabId);
|
|
748
|
-
const result = await executeInTab(
|
|
749
|
-
tabId,
|
|
750
|
-
(selectorValue) => {
|
|
751
|
-
let elements = [];
|
|
752
|
-
try {
|
|
753
|
-
elements = Array.from(document.querySelectorAll(selectorValue));
|
|
754
|
-
} catch {
|
|
755
|
-
return {
|
|
756
|
-
selector: selectorValue,
|
|
757
|
-
count: 0,
|
|
758
|
-
nodes: []
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const nodes = elements.map((element, index) => {
|
|
763
|
-
const attributes = {};
|
|
764
|
-
for (const attribute of Array.from(element.attributes)) {
|
|
765
|
-
attributes[attribute.name] = attribute.value;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return {
|
|
769
|
-
index,
|
|
770
|
-
tagName: element.tagName.toLowerCase(),
|
|
771
|
-
id: element.id || undefined,
|
|
772
|
-
className: element.className || undefined,
|
|
773
|
-
text: (element.textContent || "").trim().slice(0, 300),
|
|
774
|
-
attributes
|
|
775
|
-
};
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
return {
|
|
779
|
-
selector: selectorValue,
|
|
780
|
-
count: nodes.length,
|
|
781
|
-
nodes
|
|
782
|
-
};
|
|
783
|
-
},
|
|
784
|
-
[selector]
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
if (result !== null && typeof result === "object") {
|
|
788
|
-
return result;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
return {
|
|
792
|
-
selector,
|
|
793
|
-
count: 0,
|
|
794
|
-
nodes: []
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
async function handleTabElementScreenshot(params) {
|
|
799
|
-
const tabId = requireTabId(params?.tabId);
|
|
800
|
-
const selector = requireNonEmptyString(params?.selector, "selector");
|
|
801
|
-
await ensureDebuggerAttached(tabId);
|
|
802
|
-
const tab = await chrome.tabs.get(tabId);
|
|
803
|
-
if (typeof tab.windowId !== "number") {
|
|
804
|
-
throw new Error(`Unable to resolve window for tab ${tabId}.`);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
await chrome.windows.update(tab.windowId, { focused: true });
|
|
808
|
-
await chrome.tabs.update(tabId, { active: true });
|
|
809
|
-
const elementMeta = await executeInTab(
|
|
810
|
-
tabId,
|
|
811
|
-
(selectorValue) => {
|
|
812
|
-
let element = null;
|
|
813
|
-
try {
|
|
814
|
-
element = document.querySelector(selectorValue);
|
|
815
|
-
} catch {
|
|
816
|
-
return {
|
|
817
|
-
selector: selectorValue,
|
|
818
|
-
found: false
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if (element === null) {
|
|
823
|
-
return {
|
|
824
|
-
selector: selectorValue,
|
|
825
|
-
found: false
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
element.scrollIntoView({
|
|
830
|
-
block: "center",
|
|
831
|
-
inline: "center",
|
|
832
|
-
behavior: "auto"
|
|
833
|
-
});
|
|
834
|
-
const rect = element.getBoundingClientRect();
|
|
835
|
-
const dpr = typeof window.devicePixelRatio === "number" ? window.devicePixelRatio : 1;
|
|
836
|
-
return {
|
|
837
|
-
selector: selectorValue,
|
|
838
|
-
found: true,
|
|
839
|
-
x: rect.left * dpr,
|
|
840
|
-
y: rect.top * dpr,
|
|
841
|
-
width: Math.max(1, Math.round(rect.width * dpr)),
|
|
842
|
-
height: Math.max(1, Math.round(rect.height * dpr))
|
|
843
|
-
};
|
|
844
|
-
},
|
|
845
|
-
[selector]
|
|
846
|
-
);
|
|
847
|
-
|
|
848
|
-
if (elementMeta === null || typeof elementMeta !== "object" || elementMeta.found !== true) {
|
|
849
|
-
return {
|
|
850
|
-
selector,
|
|
851
|
-
found: false
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
const rawDataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
|
|
856
|
-
format: "png"
|
|
857
|
-
});
|
|
858
|
-
const parsedImage = await cropCapturedImageDataUrl(rawDataUrl, elementMeta);
|
|
859
|
-
return {
|
|
860
|
-
selector,
|
|
861
|
-
found: true,
|
|
862
|
-
mimeType: parsedImage.mimeType,
|
|
863
|
-
encoding: "base64",
|
|
864
|
-
imageBase64: parsedImage.imageBase64,
|
|
865
|
-
...(typeof elementMeta.width === "number" ? { width: elementMeta.width } : {}),
|
|
866
|
-
...(typeof elementMeta.height === "number" ? { height: elementMeta.height } : {})
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
async function handleTabA11ySnapshot(params) {
|
|
871
|
-
const tabId = requireTabId(params?.tabId);
|
|
872
|
-
const selector = normalizeString(params?.selector);
|
|
873
|
-
await ensureDebuggerAttached(tabId);
|
|
874
|
-
const result = await executeInTab(
|
|
875
|
-
tabId,
|
|
876
|
-
(selectorValue) => {
|
|
877
|
-
const root =
|
|
878
|
-
selectorValue.length > 0
|
|
879
|
-
? document.querySelector(selectorValue)
|
|
880
|
-
: document.body ?? document.documentElement;
|
|
881
|
-
if (root === null) {
|
|
882
|
-
return {
|
|
883
|
-
selector: selectorValue,
|
|
884
|
-
found: false
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const build = (element, depth) => {
|
|
889
|
-
const role =
|
|
890
|
-
element.getAttribute("role") ||
|
|
891
|
-
(element.tagName.toLowerCase() === "a"
|
|
892
|
-
? "link"
|
|
893
|
-
: element.tagName.toLowerCase() === "button"
|
|
894
|
-
? "button"
|
|
895
|
-
: element.tagName.toLowerCase() === "input"
|
|
896
|
-
? "textbox"
|
|
897
|
-
: "generic");
|
|
898
|
-
const name =
|
|
899
|
-
element.getAttribute("aria-label") ||
|
|
900
|
-
element.getAttribute("alt") ||
|
|
901
|
-
(element.textContent || "").trim().slice(0, 120);
|
|
902
|
-
if (depth >= 5) {
|
|
903
|
-
return {
|
|
904
|
-
role,
|
|
905
|
-
...(name.length > 0 ? { name } : {})
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
const children = Array.from(element.children)
|
|
910
|
-
.slice(0, 30)
|
|
911
|
-
.map((child) => build(child, depth + 1));
|
|
912
|
-
return {
|
|
913
|
-
role,
|
|
914
|
-
...(name.length > 0 ? { name } : {}),
|
|
915
|
-
...(children.length > 0 ? { children } : {})
|
|
916
|
-
};
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
return {
|
|
920
|
-
selector: selectorValue,
|
|
921
|
-
found: true,
|
|
922
|
-
snapshot: build(root, 0)
|
|
923
|
-
};
|
|
924
|
-
},
|
|
925
|
-
[selector]
|
|
926
|
-
);
|
|
927
|
-
|
|
928
|
-
if (result !== null && typeof result === "object") {
|
|
929
|
-
return result;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
return {
|
|
933
|
-
selector,
|
|
934
|
-
found: false
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function toCookieSummary(cookie) {
|
|
939
|
-
return {
|
|
940
|
-
name: typeof cookie.name === "string" ? cookie.name : "",
|
|
941
|
-
value: typeof cookie.value === "string" ? cookie.value : "",
|
|
942
|
-
...(typeof cookie.domain === "string" ? { domain: cookie.domain } : {}),
|
|
943
|
-
...(typeof cookie.path === "string" ? { path: cookie.path } : {}),
|
|
944
|
-
...(typeof cookie.expires === "number" ? { expires: cookie.expires } : {}),
|
|
945
|
-
...(typeof cookie.httpOnly === "boolean" ? { httpOnly: cookie.httpOnly } : {}),
|
|
946
|
-
...(typeof cookie.secure === "boolean" ? { secure: cookie.secure } : {}),
|
|
947
|
-
...(typeof cookie.sameSite === "string" ? { sameSite: cookie.sameSite } : {})
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function toCookieRemovalUrl(cookie, fallbackUrl) {
|
|
952
|
-
if (typeof fallbackUrl === "string" && fallbackUrl.length > 0) {
|
|
953
|
-
return fallbackUrl;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
const domain = normalizeString(cookie?.domain).replace(/^\./, "");
|
|
957
|
-
const path = normalizeString(cookie?.path) || "/";
|
|
958
|
-
const secure = cookie?.secure === true;
|
|
959
|
-
if (domain.length === 0) {
|
|
960
|
-
throw new Error("Unable to resolve cookie domain for removal.");
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
return `${secure ? "https" : "http"}://${domain}${path}`;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
async function handleTabCookieGet(params) {
|
|
967
|
-
const tabId = requireTabId(params?.tabId);
|
|
968
|
-
await ensureDebuggerAttached(tabId);
|
|
969
|
-
const tab = await chrome.tabs.get(tabId);
|
|
970
|
-
const tabUrl = requireNonEmptyString(tab?.url, "tab.url");
|
|
971
|
-
const name = normalizeString(params?.name);
|
|
972
|
-
const cookies = await chrome.cookies.getAll({
|
|
973
|
-
url: tabUrl,
|
|
974
|
-
...(name.length > 0 ? { name } : {})
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
return {
|
|
978
|
-
cookies: cookies.map((cookie) => toCookieSummary(cookie)),
|
|
979
|
-
...(name.length > 0 ? { name } : {})
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
async function handleTabCookieSet(params) {
|
|
984
|
-
const tabId = requireTabId(params?.tabId);
|
|
985
|
-
await ensureDebuggerAttached(tabId);
|
|
986
|
-
const tab = await chrome.tabs.get(tabId);
|
|
987
|
-
const tabUrl = requireNonEmptyString(tab?.url, "tab.url");
|
|
988
|
-
const name = requireNonEmptyString(params?.name, "name");
|
|
989
|
-
const value = requireNonEmptyString(params?.value, "value");
|
|
990
|
-
const cookieUrl = normalizeString(params?.url) || tabUrl;
|
|
991
|
-
|
|
992
|
-
const cookie = await chrome.cookies.set({
|
|
993
|
-
url: cookieUrl,
|
|
994
|
-
name,
|
|
995
|
-
value
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
return {
|
|
999
|
-
set: true,
|
|
1000
|
-
cookie: cookie !== null && cookie !== undefined
|
|
1001
|
-
? toCookieSummary(cookie)
|
|
1002
|
-
: {
|
|
1003
|
-
name,
|
|
1004
|
-
value
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
async function handleTabCookieClear(params) {
|
|
1010
|
-
const tabId = requireTabId(params?.tabId);
|
|
1011
|
-
await ensureDebuggerAttached(tabId);
|
|
1012
|
-
const tab = await chrome.tabs.get(tabId);
|
|
1013
|
-
const tabUrl = requireNonEmptyString(tab?.url, "tab.url");
|
|
1014
|
-
const name = normalizeString(params?.name);
|
|
1015
|
-
const cookies = await chrome.cookies.getAll({
|
|
1016
|
-
url: tabUrl,
|
|
1017
|
-
...(name.length > 0 ? { name } : {})
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
let removed = 0;
|
|
1021
|
-
for (const cookie of cookies) {
|
|
1022
|
-
const removeResult = await chrome.cookies.remove({
|
|
1023
|
-
url: toCookieRemovalUrl(cookie, tabUrl),
|
|
1024
|
-
name: cookie.name,
|
|
1025
|
-
...(typeof cookie.storeId === "string" ? { storeId: cookie.storeId } : {})
|
|
1026
|
-
});
|
|
1027
|
-
if (removeResult !== null && removeResult !== undefined) {
|
|
1028
|
-
removed += 1;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
return {
|
|
1033
|
-
cleared: true,
|
|
1034
|
-
count: removed,
|
|
1035
|
-
...(name.length > 0 ? { name } : {})
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
async function handleTabStorageGet(params) {
|
|
1040
|
-
const tabId = requireTabId(params?.tabId);
|
|
1041
|
-
const scope = requireNonEmptyString(params?.scope, "scope");
|
|
1042
|
-
if (scope !== "local" && scope !== "session") {
|
|
1043
|
-
throw new Error("scope must be local or session.");
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const key = requireNonEmptyString(params?.key, "key");
|
|
1047
|
-
await ensureDebuggerAttached(tabId);
|
|
1048
|
-
const result = await executeInTab(
|
|
1049
|
-
tabId,
|
|
1050
|
-
(scopeValue, keyValue) => {
|
|
1051
|
-
const storage = scopeValue === "session" ? window.sessionStorage : window.localStorage;
|
|
1052
|
-
const value = storage.getItem(keyValue);
|
|
1053
|
-
return {
|
|
1054
|
-
scope: scopeValue,
|
|
1055
|
-
key: keyValue,
|
|
1056
|
-
exists: value !== null,
|
|
1057
|
-
...(value !== null ? { value } : {})
|
|
1058
|
-
};
|
|
1059
|
-
},
|
|
1060
|
-
[scope, key]
|
|
1061
|
-
);
|
|
1062
|
-
|
|
1063
|
-
if (result !== null && typeof result === "object") {
|
|
1064
|
-
return result;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
return {
|
|
1068
|
-
scope,
|
|
1069
|
-
key,
|
|
1070
|
-
exists: false
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
async function handleTabStorageSet(params) {
|
|
1075
|
-
const tabId = requireTabId(params?.tabId);
|
|
1076
|
-
const scope = requireNonEmptyString(params?.scope, "scope");
|
|
1077
|
-
if (scope !== "local" && scope !== "session") {
|
|
1078
|
-
throw new Error("scope must be local or session.");
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
const key = requireNonEmptyString(params?.key, "key");
|
|
1082
|
-
const value = requireNonEmptyString(params?.value, "value");
|
|
1083
|
-
await ensureDebuggerAttached(tabId);
|
|
1084
|
-
await executeInTab(
|
|
1085
|
-
tabId,
|
|
1086
|
-
(scopeValue, keyValue, valueText) => {
|
|
1087
|
-
const storage = scopeValue === "session" ? window.sessionStorage : window.localStorage;
|
|
1088
|
-
storage.setItem(keyValue, valueText);
|
|
1089
|
-
},
|
|
1090
|
-
[scope, key, value]
|
|
1091
|
-
);
|
|
1092
|
-
|
|
1093
|
-
return {
|
|
1094
|
-
scope,
|
|
1095
|
-
key,
|
|
1096
|
-
value,
|
|
1097
|
-
set: true
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
async function handleTabFrameList(params) {
|
|
1102
|
-
const tabId = requireTabId(params?.tabId);
|
|
1103
|
-
await ensureDebuggerAttached(tabId);
|
|
1104
|
-
const result = await executeInTab(
|
|
1105
|
-
tabId,
|
|
1106
|
-
() => {
|
|
1107
|
-
const frames = [];
|
|
1108
|
-
|
|
1109
|
-
const walk = (currentWindow, framePath, depth) => {
|
|
1110
|
-
let url = "";
|
|
1111
|
-
let title = "";
|
|
1112
|
-
try {
|
|
1113
|
-
url = String(currentWindow.location.href || "");
|
|
1114
|
-
title = String(currentWindow.document.title || "");
|
|
1115
|
-
} catch {
|
|
1116
|
-
// cross-origin frame
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
frames.push({
|
|
1120
|
-
frameId: framePath,
|
|
1121
|
-
url,
|
|
1122
|
-
...(title.length > 0 ? { name: title } : {}),
|
|
1123
|
-
isMainFrame: framePath === "frame:0",
|
|
1124
|
-
depth
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
let childCount = 0;
|
|
1128
|
-
try {
|
|
1129
|
-
childCount = currentWindow.frames.length;
|
|
1130
|
-
} catch {
|
|
1131
|
-
childCount = 0;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
for (let index = 0; index < childCount; index += 1) {
|
|
1135
|
-
try {
|
|
1136
|
-
walk(currentWindow.frames[index], `${framePath}.${index}`, depth + 1);
|
|
1137
|
-
} catch {
|
|
1138
|
-
frames.push({
|
|
1139
|
-
frameId: `${framePath}.${index}`,
|
|
1140
|
-
url: "",
|
|
1141
|
-
isMainFrame: false,
|
|
1142
|
-
depth: depth + 1,
|
|
1143
|
-
crossOrigin: true
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
};
|
|
1148
|
-
|
|
1149
|
-
walk(window, "frame:0", 0);
|
|
1150
|
-
return {
|
|
1151
|
-
frames
|
|
1152
|
-
};
|
|
1153
|
-
},
|
|
1154
|
-
[]
|
|
1155
|
-
);
|
|
1156
|
-
|
|
1157
|
-
if (result !== null && typeof result === "object") {
|
|
1158
|
-
return result;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
return {
|
|
1162
|
-
frames: []
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
async function handleTabFrameSnapshot(params) {
|
|
1167
|
-
const tabId = requireTabId(params?.tabId);
|
|
1168
|
-
const frameId = requireNonEmptyString(params?.frameId, "frameId");
|
|
1169
|
-
await ensureDebuggerAttached(tabId);
|
|
1170
|
-
const result = await executeInTab(
|
|
1171
|
-
tabId,
|
|
1172
|
-
(frameIdValue) => {
|
|
1173
|
-
const normalized = String(frameIdValue || "");
|
|
1174
|
-
const match = /^frame:(\d+(?:\.\d+)*)$/.exec(normalized);
|
|
1175
|
-
if (match === null) {
|
|
1176
|
-
return {
|
|
1177
|
-
frameId: normalized,
|
|
1178
|
-
found: false
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
const indexPath = match[1]
|
|
1183
|
-
.split(".")
|
|
1184
|
-
.map((segment) => Number.parseInt(segment, 10))
|
|
1185
|
-
.filter((segment) => Number.isFinite(segment));
|
|
1186
|
-
if (indexPath.length === 0 || indexPath[0] !== 0) {
|
|
1187
|
-
return {
|
|
1188
|
-
frameId: normalized,
|
|
1189
|
-
found: false
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
let currentWindow = window;
|
|
1194
|
-
for (let pointer = 1; pointer < indexPath.length; pointer += 1) {
|
|
1195
|
-
const childIndex = indexPath[pointer];
|
|
1196
|
-
if (!Number.isFinite(childIndex) || childIndex < 0 || childIndex >= currentWindow.frames.length) {
|
|
1197
|
-
return {
|
|
1198
|
-
frameId: normalized,
|
|
1199
|
-
found: false
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
currentWindow = currentWindow.frames[childIndex];
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
try {
|
|
1207
|
-
const doc = currentWindow.document;
|
|
1208
|
-
return {
|
|
1209
|
-
frameId: normalized,
|
|
1210
|
-
found: true,
|
|
1211
|
-
url: String(currentWindow.location.href || ""),
|
|
1212
|
-
...(String(doc.title || "").length > 0 ? { name: String(doc.title) } : {}),
|
|
1213
|
-
html: doc.documentElement ? doc.documentElement.outerHTML : "",
|
|
1214
|
-
crossOrigin: false
|
|
1215
|
-
};
|
|
1216
|
-
} catch {
|
|
1217
|
-
return {
|
|
1218
|
-
frameId: normalized,
|
|
1219
|
-
found: true,
|
|
1220
|
-
html: "",
|
|
1221
|
-
crossOrigin: true
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
},
|
|
1225
|
-
[frameId]
|
|
1226
|
-
);
|
|
1227
|
-
|
|
1228
|
-
if (result !== null && typeof result === "object") {
|
|
1229
|
-
return result;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
return {
|
|
1233
|
-
frameId,
|
|
1234
|
-
found: false
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
async function handleTabAct(params) {
|
|
1239
|
-
const tabId = requireTabId(params?.tabId);
|
|
1240
|
-
const debuggerReady = await ensureDebuggerAttached(tabId);
|
|
1241
|
-
const action = params?.action;
|
|
1242
|
-
const actionType = normalizeString(action?.type);
|
|
1243
|
-
|
|
1244
|
-
if (actionType === "setFiles") {
|
|
1245
|
-
if (!debuggerReady) {
|
|
1246
|
-
throw new Error(
|
|
1247
|
-
`Unable to attach debugger for tab ${tabId}. Close DevTools/other debugger and retry file upload.`
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
const payload = action?.payload !== null && typeof action?.payload === "object" ? action.payload : {};
|
|
1252
|
-
const selector = requireNonEmptyString(payload?.selector, "selector");
|
|
1253
|
-
const files = Array.isArray(payload?.files)
|
|
1254
|
-
? payload.files
|
|
1255
|
-
.map((file) => normalizeString(file))
|
|
1256
|
-
.filter((file) => file.length > 0)
|
|
1257
|
-
: [];
|
|
1258
|
-
if (files.length === 0) {
|
|
1259
|
-
throw new Error("files must contain at least one file path.");
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
const evaluateResult = await chrome.debugger.sendCommand(
|
|
1263
|
-
{ tabId },
|
|
1264
|
-
"Runtime.evaluate",
|
|
1265
|
-
{
|
|
1266
|
-
expression: `document.querySelector(${JSON.stringify(selector)})`,
|
|
1267
|
-
objectGroup: "browserctl-upload",
|
|
1268
|
-
includeCommandLineAPI: false,
|
|
1269
|
-
silent: true
|
|
1270
|
-
}
|
|
1271
|
-
);
|
|
1272
|
-
const objectId = evaluateResult?.result?.objectId;
|
|
1273
|
-
if (typeof objectId !== "string" || objectId.length === 0) {
|
|
1274
|
-
throw new Error(`Element not found for file upload selector: ${selector}`);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
let nodeId;
|
|
1278
|
-
try {
|
|
1279
|
-
const nodeResult = await chrome.debugger.sendCommand(
|
|
1280
|
-
{ tabId },
|
|
1281
|
-
"DOM.requestNode",
|
|
1282
|
-
{ objectId }
|
|
1283
|
-
);
|
|
1284
|
-
nodeId = nodeResult?.nodeId;
|
|
1285
|
-
} finally {
|
|
1286
|
-
try {
|
|
1287
|
-
await chrome.debugger.sendCommand(
|
|
1288
|
-
{ tabId },
|
|
1289
|
-
"Runtime.releaseObject",
|
|
1290
|
-
{ objectId }
|
|
1291
|
-
);
|
|
1292
|
-
} catch {
|
|
1293
|
-
// Ignore release errors.
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
if (typeof nodeId !== "number") {
|
|
1298
|
-
throw new Error(`Unable to resolve DOM node for selector: ${selector}`);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
await chrome.debugger.sendCommand(
|
|
1302
|
-
{ tabId },
|
|
1303
|
-
"DOM.setFileInputFiles",
|
|
1304
|
-
{
|
|
1305
|
-
nodeId,
|
|
1306
|
-
files
|
|
1307
|
-
}
|
|
1308
|
-
);
|
|
1309
|
-
|
|
1310
|
-
return { executed: true };
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
const result = await executeInTab(
|
|
1314
|
-
tabId,
|
|
1315
|
-
(actionPayload) => {
|
|
1316
|
-
if (actionPayload === null || typeof actionPayload !== "object") {
|
|
1317
|
-
throw new Error("action must be an object.");
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
const type =
|
|
1321
|
-
typeof actionPayload.type === "string" && actionPayload.type.trim().length > 0
|
|
1322
|
-
? actionPayload.type.trim()
|
|
1323
|
-
: "";
|
|
1324
|
-
if (type.length === 0) {
|
|
1325
|
-
throw new Error("action.type must be a non-empty string.");
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const payload =
|
|
1329
|
-
actionPayload.payload !== null && typeof actionPayload.payload === "object"
|
|
1330
|
-
? actionPayload.payload
|
|
1331
|
-
: {};
|
|
1332
|
-
const selector =
|
|
1333
|
-
typeof payload.selector === "string" && payload.selector.trim().length > 0
|
|
1334
|
-
? payload.selector.trim()
|
|
1335
|
-
: "";
|
|
1336
|
-
|
|
1337
|
-
const resolveElement = () => {
|
|
1338
|
-
if (selector.length === 0) {
|
|
1339
|
-
throw new Error("selector is required.");
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const element = document.querySelector(selector);
|
|
1343
|
-
if (element === null) {
|
|
1344
|
-
throw new Error(`Element not found: ${selector}`);
|
|
1345
|
-
}
|
|
1346
|
-
return element;
|
|
1347
|
-
};
|
|
1348
|
-
|
|
1349
|
-
if (type === "click") {
|
|
1350
|
-
const element = resolveElement();
|
|
1351
|
-
if (typeof element.click === "function") {
|
|
1352
|
-
element.click();
|
|
1353
|
-
}
|
|
1354
|
-
return { executed: true };
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
if (type === "fill") {
|
|
1358
|
-
const element = resolveElement();
|
|
1359
|
-
const value = typeof payload.value === "string" ? payload.value : "";
|
|
1360
|
-
if ("value" in element) {
|
|
1361
|
-
element.value = value;
|
|
1362
|
-
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1363
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1364
|
-
}
|
|
1365
|
-
return { executed: true };
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
if (type === "type") {
|
|
1369
|
-
const element = resolveElement();
|
|
1370
|
-
const text = typeof payload.text === "string" ? payload.text : "";
|
|
1371
|
-
if ("value" in element) {
|
|
1372
|
-
const previousValue = typeof element.value === "string" ? element.value : "";
|
|
1373
|
-
element.value = `${previousValue}${text}`;
|
|
1374
|
-
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1375
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1376
|
-
}
|
|
1377
|
-
return { executed: true };
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (type === "press") {
|
|
1381
|
-
const key =
|
|
1382
|
-
typeof payload.key === "string" && payload.key.trim().length > 0
|
|
1383
|
-
? payload.key.trim()
|
|
1384
|
-
: "Enter";
|
|
1385
|
-
const target = document.activeElement ?? document.body;
|
|
1386
|
-
target.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true }));
|
|
1387
|
-
target.dispatchEvent(new KeyboardEvent("keyup", { key, bubbles: true }));
|
|
1388
|
-
return { executed: true };
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
if (type === "scroll") {
|
|
1392
|
-
const deltaX =
|
|
1393
|
-
typeof payload.deltaX === "number" && Number.isFinite(payload.deltaX)
|
|
1394
|
-
? payload.deltaX
|
|
1395
|
-
: 0;
|
|
1396
|
-
const deltaY =
|
|
1397
|
-
typeof payload.deltaY === "number" && Number.isFinite(payload.deltaY)
|
|
1398
|
-
? payload.deltaY
|
|
1399
|
-
: 900;
|
|
1400
|
-
window.scrollBy({
|
|
1401
|
-
left: deltaX,
|
|
1402
|
-
top: deltaY
|
|
1403
|
-
});
|
|
1404
|
-
return {
|
|
1405
|
-
executed: true,
|
|
1406
|
-
deltaX,
|
|
1407
|
-
deltaY,
|
|
1408
|
-
scrollX: window.scrollX,
|
|
1409
|
-
scrollY: window.scrollY
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
return {
|
|
1414
|
-
executed: false
|
|
1415
|
-
};
|
|
1416
|
-
},
|
|
1417
|
-
[action]
|
|
1418
|
-
);
|
|
1419
|
-
|
|
1420
|
-
if (result !== null && typeof result === "object") {
|
|
1421
|
-
return result;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
return {
|
|
1425
|
-
executed: false
|
|
1426
|
-
};
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
async function dispatchBridgeRequest(method, params) {
|
|
1430
|
-
switch (method) {
|
|
1431
|
-
case "tab.open":
|
|
1432
|
-
return await handleTabOpen(params);
|
|
1433
|
-
case "tab.goto":
|
|
1434
|
-
return await handleTabGoto(params);
|
|
1435
|
-
case "tab.focus":
|
|
1436
|
-
return await handleTabFocus(params);
|
|
1437
|
-
case "tab.close":
|
|
1438
|
-
return await handleTabClose(params);
|
|
1439
|
-
case "tab.info":
|
|
1440
|
-
return await handleTabInfo(params);
|
|
1441
|
-
case "tab.snapshot":
|
|
1442
|
-
return await handleTabSnapshot(params);
|
|
1443
|
-
case "tab.dom.query":
|
|
1444
|
-
return await handleTabDomQuery(params);
|
|
1445
|
-
case "tab.dom.queryAll":
|
|
1446
|
-
return await handleTabDomQueryAll(params);
|
|
1447
|
-
case "tab.screenshot":
|
|
1448
|
-
return await handleTabScreenshot(params);
|
|
1449
|
-
case "tab.element.screenshot":
|
|
1450
|
-
return await handleTabElementScreenshot(params);
|
|
1451
|
-
case "tab.a11y.snapshot":
|
|
1452
|
-
return await handleTabA11ySnapshot(params);
|
|
1453
|
-
case "tab.cookie.get":
|
|
1454
|
-
return await handleTabCookieGet(params);
|
|
1455
|
-
case "tab.cookie.set":
|
|
1456
|
-
return await handleTabCookieSet(params);
|
|
1457
|
-
case "tab.cookie.clear":
|
|
1458
|
-
return await handleTabCookieClear(params);
|
|
1459
|
-
case "tab.storage.get":
|
|
1460
|
-
return await handleTabStorageGet(params);
|
|
1461
|
-
case "tab.storage.set":
|
|
1462
|
-
return await handleTabStorageSet(params);
|
|
1463
|
-
case "tab.frame.list":
|
|
1464
|
-
return await handleTabFrameList(params);
|
|
1465
|
-
case "tab.frame.snapshot":
|
|
1466
|
-
return await handleTabFrameSnapshot(params);
|
|
1467
|
-
case "tab.act":
|
|
1468
|
-
return await handleTabAct(params);
|
|
1469
|
-
default:
|
|
1470
|
-
throw new Error(`Unsupported method: ${method}`);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
async function handleSocketMessage(rawMessage) {
|
|
1475
|
-
let parsedMessage;
|
|
1476
|
-
try {
|
|
1477
|
-
parsedMessage = JSON.parse(String(rawMessage));
|
|
1478
|
-
} catch {
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
if (parsedMessage === null || typeof parsedMessage !== "object") {
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
if (parsedMessage.type !== "request") {
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const requestId = normalizeString(parsedMessage.id);
|
|
1491
|
-
const method = normalizeString(parsedMessage.method);
|
|
1492
|
-
if (requestId.length === 0 || method.length === 0) {
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
try {
|
|
1497
|
-
const result = await dispatchBridgeRequest(method, parsedMessage.params);
|
|
1498
|
-
sendSocketEnvelope({
|
|
1499
|
-
type: "response",
|
|
1500
|
-
id: requestId,
|
|
1501
|
-
ok: true,
|
|
1502
|
-
result
|
|
1503
|
-
});
|
|
1504
|
-
} catch (error) {
|
|
1505
|
-
sendSocketEnvelope({
|
|
1506
|
-
type: "response",
|
|
1507
|
-
id: requestId,
|
|
1508
|
-
ok: false,
|
|
1509
|
-
error: {
|
|
1510
|
-
message: toErrorMessage(error)
|
|
1511
|
-
}
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
async function connectToBridge(forceReconnect) {
|
|
1517
|
-
if (connectInFlight) {
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
if (
|
|
1522
|
-
!forceReconnect &&
|
|
1523
|
-
socket !== null &&
|
|
1524
|
-
(socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)
|
|
1525
|
-
) {
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
connectInFlight = true;
|
|
1530
|
-
clearReconnectTimer();
|
|
1531
|
-
|
|
1532
|
-
try {
|
|
1533
|
-
const config = await readConfig();
|
|
1534
|
-
const socketUrl = buildSocketUrl(config);
|
|
1535
|
-
|
|
1536
|
-
if (socket !== null) {
|
|
1537
|
-
try {
|
|
1538
|
-
socket.close(1000, "Reconnect requested.");
|
|
1539
|
-
} catch {
|
|
1540
|
-
// Ignore close errors while reconnecting.
|
|
1541
|
-
}
|
|
1542
|
-
socket = null;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const ws = new WebSocket(socketUrl);
|
|
1546
|
-
socket = ws;
|
|
1547
|
-
runtimeState.bridgeUrl = config.bridgeUrl;
|
|
1548
|
-
runtimeState.token = config.token;
|
|
1549
|
-
|
|
1550
|
-
ws.onopen = () => {
|
|
1551
|
-
runtimeState.connected = true;
|
|
1552
|
-
runtimeState.lastError = "";
|
|
1553
|
-
runtimeState.lastConnectedAt = new Date().toISOString();
|
|
1554
|
-
connectInFlight = false;
|
|
1555
|
-
sendHello();
|
|
1556
|
-
};
|
|
1557
|
-
|
|
1558
|
-
ws.onmessage = (event) => {
|
|
1559
|
-
void handleSocketMessage(event.data);
|
|
1560
|
-
};
|
|
1561
|
-
|
|
1562
|
-
ws.onerror = () => {
|
|
1563
|
-
runtimeState.lastError = "WebSocket connection error.";
|
|
1564
|
-
};
|
|
1565
|
-
|
|
1566
|
-
ws.onclose = (event) => {
|
|
1567
|
-
if (socket === ws) {
|
|
1568
|
-
socket = null;
|
|
1569
|
-
}
|
|
1570
|
-
connectInFlight = false;
|
|
1571
|
-
setDisconnectedState(`WebSocket closed (${event.code}).`);
|
|
1572
|
-
scheduleReconnect();
|
|
1573
|
-
};
|
|
1574
|
-
} catch (error) {
|
|
1575
|
-
connectInFlight = false;
|
|
1576
|
-
setDisconnectedState(toErrorMessage(error));
|
|
1577
|
-
scheduleReconnect();
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
function getStatusPayload() {
|
|
1582
|
-
return {
|
|
1583
|
-
connected: runtimeState.connected,
|
|
1584
|
-
bridgeUrl: runtimeState.bridgeUrl,
|
|
1585
|
-
tokenConfigured: runtimeState.token.length > 0,
|
|
1586
|
-
lastConnectedAt: runtimeState.lastConnectedAt,
|
|
1587
|
-
lastError: runtimeState.lastError,
|
|
1588
|
-
debuggerAttachedTabs: attachedDebuggerTabs.size
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
function registerRuntimeMessageHandlers() {
|
|
1593
|
-
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
1594
|
-
const requestType = normalizeString(request?.type);
|
|
1595
|
-
|
|
1596
|
-
if (requestType === "relay.getStatus") {
|
|
1597
|
-
sendResponse({
|
|
1598
|
-
ok: true,
|
|
1599
|
-
status: getStatusPayload()
|
|
1600
|
-
});
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
if (requestType === "relay.getConfig") {
|
|
1605
|
-
void (async () => {
|
|
1606
|
-
const config = await readConfig();
|
|
1607
|
-
sendResponse({
|
|
1608
|
-
ok: true,
|
|
1609
|
-
config
|
|
1610
|
-
});
|
|
1611
|
-
})();
|
|
1612
|
-
return true;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
if (requestType === "relay.saveConfig") {
|
|
1616
|
-
void (async () => {
|
|
1617
|
-
try {
|
|
1618
|
-
const config = await writeConfig(request?.config ?? {});
|
|
1619
|
-
await connectToBridge(true);
|
|
1620
|
-
sendResponse({
|
|
1621
|
-
ok: true,
|
|
1622
|
-
config,
|
|
1623
|
-
status: getStatusPayload()
|
|
1624
|
-
});
|
|
1625
|
-
} catch (error) {
|
|
1626
|
-
sendResponse({
|
|
1627
|
-
ok: false,
|
|
1628
|
-
error: toErrorMessage(error)
|
|
1629
|
-
});
|
|
1630
|
-
}
|
|
1631
|
-
})();
|
|
1632
|
-
return true;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
if (requestType === "relay.reconnect") {
|
|
1636
|
-
void (async () => {
|
|
1637
|
-
try {
|
|
1638
|
-
await connectToBridge(true);
|
|
1639
|
-
sendResponse({
|
|
1640
|
-
ok: true,
|
|
1641
|
-
status: getStatusPayload()
|
|
1642
|
-
});
|
|
1643
|
-
} catch (error) {
|
|
1644
|
-
sendResponse({
|
|
1645
|
-
ok: false,
|
|
1646
|
-
error: toErrorMessage(error)
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
})();
|
|
1650
|
-
return true;
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (requestType === "relay.event") {
|
|
1654
|
-
const tabId =
|
|
1655
|
-
typeof request?.tabId === "number"
|
|
1656
|
-
? request.tabId
|
|
1657
|
-
: typeof sender?.tab?.id === "number"
|
|
1658
|
-
? sender.tab.id
|
|
1659
|
-
: undefined;
|
|
1660
|
-
if (typeof tabId === "number" && request?.event !== undefined) {
|
|
1661
|
-
sendBridgeEvent({
|
|
1662
|
-
...request.event,
|
|
1663
|
-
tabId
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
sendResponse({
|
|
1668
|
-
ok: true
|
|
1669
|
-
});
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
return undefined;
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
chrome.runtime.onInstalled.addListener(() => {
|
|
1678
|
-
void connectToBridge(true);
|
|
1679
|
-
});
|
|
1680
|
-
|
|
1681
|
-
chrome.runtime.onStartup.addListener(() => {
|
|
1682
|
-
void connectToBridge(false);
|
|
1683
|
-
});
|
|
1684
|
-
|
|
1685
|
-
registerRuntimeMessageHandlers();
|
|
1686
|
-
registerDebuggerTelemetryHandlers();
|
|
1687
|
-
void connectToBridge(false);
|