bb-browser 0.1.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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/chunk-KATHFDYJ.js +20 -0
- package/dist/chunk-KATHFDYJ.js.map +1 -0
- package/dist/cli.js +1800 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +441 -0
- package/dist/daemon.js.map +1 -0
- package/extension/background.js +2943 -0
- package/extension/background.js.map +1 -0
- package/extension/buildDomTree.js +1505 -0
- package/extension/content/trace.js +339 -0
- package/extension/content/trace.js.map +1 -0
- package/extension/dist/background.js +2943 -0
- package/extension/dist/background.js.map +1 -0
- package/extension/dist/buildDomTree.js +1505 -0
- package/extension/dist/content/trace.js +339 -0
- package/extension/dist/content/trace.js.map +1 -0
- package/extension/dist/manifest.json +25 -0
- package/extension/manifest.json +25 -0
- package/package.json +60 -0
|
@@ -0,0 +1,2943 @@
|
|
|
1
|
+
const DAEMON_PORT = 19824;
|
|
2
|
+
const DAEMON_HOST = "localhost";
|
|
3
|
+
const DAEMON_BASE_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
4
|
+
const SSE_RECONNECT_DELAY = 3e3;
|
|
5
|
+
const SSE_MAX_RECONNECT_ATTEMPTS = 5;
|
|
6
|
+
|
|
7
|
+
class SSEClient {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.abortController = null;
|
|
10
|
+
this.reconnectAttempts = 0;
|
|
11
|
+
this.isConnectedFlag = false;
|
|
12
|
+
this.onCommandHandler = null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 连接到 Daemon SSE 端点
|
|
16
|
+
*/
|
|
17
|
+
async connect() {
|
|
18
|
+
if (this.abortController) {
|
|
19
|
+
console.warn("[SSEClient] Already connected");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const sseUrl = `${DAEMON_BASE_URL}/sse`;
|
|
23
|
+
console.log("[SSEClient] Connecting to:", sseUrl);
|
|
24
|
+
this.abortController = new AbortController();
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(sseUrl, {
|
|
27
|
+
signal: this.abortController.signal,
|
|
28
|
+
headers: {
|
|
29
|
+
Accept: "text/event-stream",
|
|
30
|
+
"Cache-Control": "no-cache"
|
|
31
|
+
},
|
|
32
|
+
keepalive: true
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
if (!response.body) {
|
|
38
|
+
throw new Error("Response body is null");
|
|
39
|
+
}
|
|
40
|
+
const contentType = response.headers.get("Content-Type");
|
|
41
|
+
console.log("[SSEClient] Connection established, Content-Type:", contentType);
|
|
42
|
+
this.isConnectedFlag = true;
|
|
43
|
+
this.reconnectAttempts = 0;
|
|
44
|
+
await this.readStream(response.body);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
47
|
+
console.log("[SSEClient] Connection aborted");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.error("[SSEClient] Connection error:", error);
|
|
51
|
+
this.isConnectedFlag = false;
|
|
52
|
+
this.reconnect();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 读取并解析 SSE 流
|
|
57
|
+
*/
|
|
58
|
+
async readStream(body) {
|
|
59
|
+
const reader = body.getReader();
|
|
60
|
+
const decoder = new TextDecoder();
|
|
61
|
+
let buffer = "";
|
|
62
|
+
let event = "";
|
|
63
|
+
let data = "";
|
|
64
|
+
try {
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) {
|
|
68
|
+
console.log("[SSEClient] Stream ended");
|
|
69
|
+
this.isConnectedFlag = false;
|
|
70
|
+
this.reconnect();
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
buffer += decoder.decode(value, { stream: true });
|
|
74
|
+
const lines = buffer.split("\n");
|
|
75
|
+
buffer = lines.pop() || "";
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const trimmedLine = line.trim();
|
|
78
|
+
if (trimmedLine.startsWith("event:")) {
|
|
79
|
+
event = trimmedLine.substring(6).trim();
|
|
80
|
+
} else if (trimmedLine.startsWith("data:")) {
|
|
81
|
+
data = trimmedLine.substring(5).trim();
|
|
82
|
+
} else if (trimmedLine === "") {
|
|
83
|
+
if (event && data) {
|
|
84
|
+
await this.handleMessage(event, data);
|
|
85
|
+
event = "";
|
|
86
|
+
data = "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
93
|
+
console.log("[SSEClient] Stream reading aborted");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.error("[SSEClient] Stream reading error:", error);
|
|
97
|
+
this.isConnectedFlag = false;
|
|
98
|
+
this.reconnect();
|
|
99
|
+
} finally {
|
|
100
|
+
reader.releaseLock();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 处理 SSE 消息
|
|
105
|
+
*/
|
|
106
|
+
async handleMessage(event, data) {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(data);
|
|
109
|
+
switch (event) {
|
|
110
|
+
case "connected":
|
|
111
|
+
console.log("[SSEClient] Connection confirmed:", parsed);
|
|
112
|
+
break;
|
|
113
|
+
case "heartbeat":
|
|
114
|
+
console.log("[SSEClient] Heartbeat:", new Date(parsed.time * 1e3).toISOString());
|
|
115
|
+
break;
|
|
116
|
+
case "command":
|
|
117
|
+
console.log("[SSEClient] Command received:", parsed.id, parsed.action);
|
|
118
|
+
if (this.onCommandHandler) {
|
|
119
|
+
await this.onCommandHandler(parsed);
|
|
120
|
+
} else {
|
|
121
|
+
console.warn("[SSEClient] No command handler registered");
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
console.log("[SSEClient] Unknown event type:", event);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error("[SSEClient] Error handling message:", error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 指数退避重连
|
|
133
|
+
*/
|
|
134
|
+
reconnect() {
|
|
135
|
+
if (this.reconnectAttempts >= SSE_MAX_RECONNECT_ATTEMPTS) {
|
|
136
|
+
console.error("[SSEClient] Max reconnection attempts reached");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.reconnectAttempts++;
|
|
140
|
+
const delay = SSE_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts - 1);
|
|
141
|
+
console.log(
|
|
142
|
+
`[SSEClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${SSE_MAX_RECONNECT_ATTEMPTS})`
|
|
143
|
+
);
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
this.disconnect();
|
|
146
|
+
this.connect();
|
|
147
|
+
}, delay);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 注册命令处理器
|
|
151
|
+
*/
|
|
152
|
+
onCommand(handler) {
|
|
153
|
+
this.onCommandHandler = handler;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 断开连接
|
|
157
|
+
*/
|
|
158
|
+
disconnect() {
|
|
159
|
+
if (this.abortController) {
|
|
160
|
+
console.log("[SSEClient] Disconnecting...");
|
|
161
|
+
this.abortController.abort();
|
|
162
|
+
this.abortController = null;
|
|
163
|
+
this.isConnectedFlag = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 检查连接状态
|
|
168
|
+
*/
|
|
169
|
+
isConnected() {
|
|
170
|
+
return this.isConnectedFlag;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function sendResult(result) {
|
|
175
|
+
const url = `${DAEMON_BASE_URL}/result`;
|
|
176
|
+
console.log("[APIClient] Sending result:", result.id, result.success);
|
|
177
|
+
try {
|
|
178
|
+
const response = await fetch(url, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: {
|
|
181
|
+
"Content-Type": "application/json"
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify(result)
|
|
184
|
+
});
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
console.error("[APIClient] Failed to send result:", response.status, response.statusText);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const data = await response.json();
|
|
190
|
+
console.log("[APIClient] Result sent successfully:", data);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("[APIClient] Error sending result:", error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const attachedTabs = /* @__PURE__ */ new Set();
|
|
197
|
+
const pendingDialogs = /* @__PURE__ */ new Map();
|
|
198
|
+
const networkRequests = /* @__PURE__ */ new Map();
|
|
199
|
+
const consoleMessages = /* @__PURE__ */ new Map();
|
|
200
|
+
const jsErrors = /* @__PURE__ */ new Map();
|
|
201
|
+
const networkRoutes = /* @__PURE__ */ new Map();
|
|
202
|
+
const networkEnabledTabs = /* @__PURE__ */ new Set();
|
|
203
|
+
const MAX_REQUESTS = 500;
|
|
204
|
+
const MAX_CONSOLE_MESSAGES = 500;
|
|
205
|
+
const MAX_ERRORS = 100;
|
|
206
|
+
async function ensureAttached(tabId) {
|
|
207
|
+
if (attachedTabs.has(tabId)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
212
|
+
attachedTabs.add(tabId);
|
|
213
|
+
await chrome.debugger.sendCommand({ tabId }, "Page.enable");
|
|
214
|
+
await chrome.debugger.sendCommand({ tabId }, "DOM.enable");
|
|
215
|
+
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
|
|
216
|
+
console.log("[CDPService] Attached to tab:", tabId);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
219
|
+
if (msg.includes("Another debugger is already attached")) {
|
|
220
|
+
attachedTabs.add(tabId);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function sendCommand(tabId, method, params) {
|
|
227
|
+
await ensureAttached(tabId);
|
|
228
|
+
const result = await chrome.debugger.sendCommand({ tabId }, method, params);
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
async function evaluate(tabId, expression, options = {}) {
|
|
232
|
+
const result = await sendCommand(tabId, "Runtime.evaluate", {
|
|
233
|
+
expression,
|
|
234
|
+
returnByValue: options.returnByValue ?? true,
|
|
235
|
+
awaitPromise: options.awaitPromise ?? true
|
|
236
|
+
});
|
|
237
|
+
if (result.exceptionDetails) {
|
|
238
|
+
const errorMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Unknown error";
|
|
239
|
+
throw new Error(`Eval error: ${errorMsg}`);
|
|
240
|
+
}
|
|
241
|
+
return result.result?.value;
|
|
242
|
+
}
|
|
243
|
+
async function dispatchMouseEvent(tabId, type, x, y, options = {}) {
|
|
244
|
+
await sendCommand(tabId, "Input.dispatchMouseEvent", {
|
|
245
|
+
type,
|
|
246
|
+
x,
|
|
247
|
+
y,
|
|
248
|
+
button: options.button ?? "left",
|
|
249
|
+
clickCount: options.clickCount ?? 1,
|
|
250
|
+
deltaX: options.deltaX ?? 0,
|
|
251
|
+
deltaY: options.deltaY ?? 0,
|
|
252
|
+
modifiers: options.modifiers ?? 0
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async function click(tabId, x, y) {
|
|
256
|
+
await dispatchMouseEvent(tabId, "mousePressed", x, y, { button: "left", clickCount: 1 });
|
|
257
|
+
await dispatchMouseEvent(tabId, "mouseReleased", x, y, { button: "left", clickCount: 1 });
|
|
258
|
+
}
|
|
259
|
+
async function moveMouse(tabId, x, y) {
|
|
260
|
+
await dispatchMouseEvent(tabId, "mouseMoved", x, y);
|
|
261
|
+
}
|
|
262
|
+
async function scroll(tabId, x, y, deltaX, deltaY) {
|
|
263
|
+
await dispatchMouseEvent(tabId, "mouseWheel", x, y, { deltaX, deltaY });
|
|
264
|
+
}
|
|
265
|
+
async function dispatchKeyEvent(tabId, type, options = {}) {
|
|
266
|
+
await sendCommand(tabId, "Input.dispatchKeyEvent", {
|
|
267
|
+
type,
|
|
268
|
+
...options
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async function pressKey$1(tabId, key, options = {}) {
|
|
272
|
+
const keyCodeMap = {
|
|
273
|
+
Enter: 13,
|
|
274
|
+
Tab: 9,
|
|
275
|
+
Backspace: 8,
|
|
276
|
+
Escape: 27,
|
|
277
|
+
ArrowUp: 38,
|
|
278
|
+
ArrowDown: 40,
|
|
279
|
+
ArrowLeft: 37,
|
|
280
|
+
ArrowRight: 39,
|
|
281
|
+
Delete: 46,
|
|
282
|
+
Home: 36,
|
|
283
|
+
End: 35,
|
|
284
|
+
PageUp: 33,
|
|
285
|
+
PageDown: 34
|
|
286
|
+
};
|
|
287
|
+
const keyCode = keyCodeMap[key] || key.charCodeAt(0);
|
|
288
|
+
await dispatchKeyEvent(tabId, "rawKeyDown", {
|
|
289
|
+
key,
|
|
290
|
+
code: key,
|
|
291
|
+
windowsVirtualKeyCode: keyCode,
|
|
292
|
+
nativeVirtualKeyCode: keyCode,
|
|
293
|
+
modifiers: options.modifiers
|
|
294
|
+
});
|
|
295
|
+
if (key.length === 1) {
|
|
296
|
+
await dispatchKeyEvent(tabId, "char", {
|
|
297
|
+
text: key,
|
|
298
|
+
key,
|
|
299
|
+
modifiers: options.modifiers
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
await dispatchKeyEvent(tabId, "keyUp", {
|
|
303
|
+
key,
|
|
304
|
+
code: key,
|
|
305
|
+
windowsVirtualKeyCode: keyCode,
|
|
306
|
+
nativeVirtualKeyCode: keyCode,
|
|
307
|
+
modifiers: options.modifiers
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async function insertText(tabId, text) {
|
|
311
|
+
await sendCommand(tabId, "Input.insertText", { text });
|
|
312
|
+
}
|
|
313
|
+
async function handleJavaScriptDialog(tabId, accept, promptText) {
|
|
314
|
+
await sendCommand(tabId, "Page.handleJavaScriptDialog", {
|
|
315
|
+
accept,
|
|
316
|
+
promptText
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function getPendingDialog(tabId) {
|
|
320
|
+
return pendingDialogs.get(tabId);
|
|
321
|
+
}
|
|
322
|
+
async function enableNetwork(tabId) {
|
|
323
|
+
if (networkEnabledTabs.has(tabId)) return;
|
|
324
|
+
await ensureAttached(tabId);
|
|
325
|
+
await sendCommand(tabId, "Network.enable");
|
|
326
|
+
await sendCommand(tabId, "Fetch.enable", {
|
|
327
|
+
patterns: [{ urlPattern: "*" }]
|
|
328
|
+
});
|
|
329
|
+
networkEnabledTabs.add(tabId);
|
|
330
|
+
if (!networkRequests.has(tabId)) {
|
|
331
|
+
networkRequests.set(tabId, []);
|
|
332
|
+
}
|
|
333
|
+
console.log("[CDPService] Network enabled for tab:", tabId);
|
|
334
|
+
}
|
|
335
|
+
function getNetworkRequests(tabId, filter) {
|
|
336
|
+
const requests = networkRequests.get(tabId) || [];
|
|
337
|
+
if (!filter) return requests;
|
|
338
|
+
const lowerFilter = filter.toLowerCase();
|
|
339
|
+
return requests.filter(
|
|
340
|
+
(r) => r.url.toLowerCase().includes(lowerFilter) || r.method.toLowerCase().includes(lowerFilter) || r.type.toLowerCase().includes(lowerFilter)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
function clearNetworkRequests(tabId) {
|
|
344
|
+
networkRequests.set(tabId, []);
|
|
345
|
+
}
|
|
346
|
+
async function addNetworkRoute(tabId, urlPattern, options = {}) {
|
|
347
|
+
await enableNetwork(tabId);
|
|
348
|
+
const route = {
|
|
349
|
+
urlPattern,
|
|
350
|
+
action: options.abort ? "abort" : options.body ? "fulfill" : "continue",
|
|
351
|
+
body: options.body,
|
|
352
|
+
status: options.status ?? 200,
|
|
353
|
+
headers: options.headers
|
|
354
|
+
};
|
|
355
|
+
const routes = networkRoutes.get(tabId) || [];
|
|
356
|
+
const filtered = routes.filter((r) => r.urlPattern !== urlPattern);
|
|
357
|
+
filtered.push(route);
|
|
358
|
+
networkRoutes.set(tabId, filtered);
|
|
359
|
+
console.log("[CDPService] Added network route:", route);
|
|
360
|
+
}
|
|
361
|
+
function removeNetworkRoute(tabId, urlPattern) {
|
|
362
|
+
if (!urlPattern) {
|
|
363
|
+
networkRoutes.delete(tabId);
|
|
364
|
+
console.log("[CDPService] Removed all network routes for tab:", tabId);
|
|
365
|
+
} else {
|
|
366
|
+
const routes = networkRoutes.get(tabId) || [];
|
|
367
|
+
networkRoutes.set(tabId, routes.filter((r) => r.urlPattern !== urlPattern));
|
|
368
|
+
console.log("[CDPService] Removed network route:", urlPattern);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function getNetworkRoutes(tabId) {
|
|
372
|
+
return networkRoutes.get(tabId) || [];
|
|
373
|
+
}
|
|
374
|
+
async function enableConsole(tabId) {
|
|
375
|
+
await ensureAttached(tabId);
|
|
376
|
+
await sendCommand(tabId, "Runtime.enable");
|
|
377
|
+
await sendCommand(tabId, "Log.enable");
|
|
378
|
+
if (!consoleMessages.has(tabId)) {
|
|
379
|
+
consoleMessages.set(tabId, []);
|
|
380
|
+
}
|
|
381
|
+
if (!jsErrors.has(tabId)) {
|
|
382
|
+
jsErrors.set(tabId, []);
|
|
383
|
+
}
|
|
384
|
+
console.log("[CDPService] Console enabled for tab:", tabId);
|
|
385
|
+
}
|
|
386
|
+
function getConsoleMessages(tabId) {
|
|
387
|
+
return consoleMessages.get(tabId) || [];
|
|
388
|
+
}
|
|
389
|
+
function clearConsoleMessages(tabId) {
|
|
390
|
+
consoleMessages.set(tabId, []);
|
|
391
|
+
}
|
|
392
|
+
function getJSErrors(tabId) {
|
|
393
|
+
return jsErrors.get(tabId) || [];
|
|
394
|
+
}
|
|
395
|
+
function clearJSErrors(tabId) {
|
|
396
|
+
jsErrors.set(tabId, []);
|
|
397
|
+
}
|
|
398
|
+
function initEventListeners() {
|
|
399
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
400
|
+
const tabId = source.tabId;
|
|
401
|
+
if (!tabId) return;
|
|
402
|
+
if (method === "Page.javascriptDialogOpening") {
|
|
403
|
+
const dialogParams = params;
|
|
404
|
+
console.log("[CDPService] Dialog opened:", dialogParams);
|
|
405
|
+
pendingDialogs.set(tabId, dialogParams);
|
|
406
|
+
} else if (method === "Page.javascriptDialogClosed") {
|
|
407
|
+
console.log("[CDPService] Dialog closed");
|
|
408
|
+
pendingDialogs.delete(tabId);
|
|
409
|
+
} else if (method === "Network.requestWillBeSent") {
|
|
410
|
+
handleNetworkRequest(tabId, params);
|
|
411
|
+
} else if (method === "Network.responseReceived") {
|
|
412
|
+
handleNetworkResponse(tabId, params);
|
|
413
|
+
} else if (method === "Network.loadingFailed") {
|
|
414
|
+
handleNetworkFailed(tabId, params);
|
|
415
|
+
} else if (method === "Fetch.requestPaused") {
|
|
416
|
+
handleFetchPaused(tabId, params);
|
|
417
|
+
} else if (method === "Runtime.consoleAPICalled") {
|
|
418
|
+
handleConsoleAPI(tabId, params);
|
|
419
|
+
} else if (method === "Log.entryAdded") {
|
|
420
|
+
handleLogEntry(tabId, params);
|
|
421
|
+
} else if (method === "Runtime.exceptionThrown") {
|
|
422
|
+
handleException(tabId, params);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
426
|
+
if (source.tabId) {
|
|
427
|
+
cleanupTab(source.tabId);
|
|
428
|
+
console.log("[CDPService] Debugger detached from tab:", source.tabId);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
432
|
+
cleanupTab(tabId);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function cleanupTab(tabId) {
|
|
436
|
+
attachedTabs.delete(tabId);
|
|
437
|
+
pendingDialogs.delete(tabId);
|
|
438
|
+
networkRequests.delete(tabId);
|
|
439
|
+
networkRoutes.delete(tabId);
|
|
440
|
+
networkEnabledTabs.delete(tabId);
|
|
441
|
+
consoleMessages.delete(tabId);
|
|
442
|
+
jsErrors.delete(tabId);
|
|
443
|
+
}
|
|
444
|
+
function handleNetworkRequest(tabId, params) {
|
|
445
|
+
const requests = networkRequests.get(tabId) || [];
|
|
446
|
+
if (requests.length >= MAX_REQUESTS) {
|
|
447
|
+
requests.shift();
|
|
448
|
+
}
|
|
449
|
+
requests.push({
|
|
450
|
+
requestId: params.requestId,
|
|
451
|
+
url: params.request.url,
|
|
452
|
+
method: params.request.method,
|
|
453
|
+
type: params.type,
|
|
454
|
+
timestamp: params.timestamp * 1e3,
|
|
455
|
+
headers: params.request.headers,
|
|
456
|
+
postData: params.request.postData
|
|
457
|
+
});
|
|
458
|
+
networkRequests.set(tabId, requests);
|
|
459
|
+
}
|
|
460
|
+
function handleNetworkResponse(tabId, params) {
|
|
461
|
+
const requests = networkRequests.get(tabId) || [];
|
|
462
|
+
const request = requests.find((r) => r.requestId === params.requestId);
|
|
463
|
+
if (request) {
|
|
464
|
+
request.response = {
|
|
465
|
+
status: params.response.status,
|
|
466
|
+
statusText: params.response.statusText,
|
|
467
|
+
headers: params.response.headers,
|
|
468
|
+
mimeType: params.response.mimeType
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function handleNetworkFailed(tabId, params) {
|
|
473
|
+
const requests = networkRequests.get(tabId) || [];
|
|
474
|
+
const request = requests.find((r) => r.requestId === params.requestId);
|
|
475
|
+
if (request) {
|
|
476
|
+
request.failed = true;
|
|
477
|
+
request.failureReason = params.errorText;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function handleFetchPaused(tabId, params) {
|
|
481
|
+
const routes = networkRoutes.get(tabId) || [];
|
|
482
|
+
const url = params.request.url;
|
|
483
|
+
const matchedRoute = routes.find((route) => {
|
|
484
|
+
if (route.urlPattern === "*") return true;
|
|
485
|
+
if (route.urlPattern.includes("*")) {
|
|
486
|
+
const regex = new RegExp(route.urlPattern.replace(/\*/g, ".*"));
|
|
487
|
+
return regex.test(url);
|
|
488
|
+
}
|
|
489
|
+
return url.includes(route.urlPattern);
|
|
490
|
+
});
|
|
491
|
+
try {
|
|
492
|
+
if (matchedRoute) {
|
|
493
|
+
if (matchedRoute.action === "abort") {
|
|
494
|
+
await sendCommand(tabId, "Fetch.failRequest", {
|
|
495
|
+
requestId: params.requestId,
|
|
496
|
+
errorReason: "BlockedByClient"
|
|
497
|
+
});
|
|
498
|
+
console.log("[CDPService] Blocked request:", url);
|
|
499
|
+
} else if (matchedRoute.action === "fulfill") {
|
|
500
|
+
await sendCommand(tabId, "Fetch.fulfillRequest", {
|
|
501
|
+
requestId: params.requestId,
|
|
502
|
+
responseCode: matchedRoute.status || 200,
|
|
503
|
+
responseHeaders: Object.entries(matchedRoute.headers || {}).map(([name, value]) => ({ name, value })),
|
|
504
|
+
body: matchedRoute.body ? btoa(matchedRoute.body) : void 0
|
|
505
|
+
});
|
|
506
|
+
console.log("[CDPService] Fulfilled request with mock:", url);
|
|
507
|
+
} else {
|
|
508
|
+
await sendCommand(tabId, "Fetch.continueRequest", {
|
|
509
|
+
requestId: params.requestId
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
await sendCommand(tabId, "Fetch.continueRequest", {
|
|
514
|
+
requestId: params.requestId
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.error("[CDPService] Fetch handling error:", error);
|
|
519
|
+
try {
|
|
520
|
+
await sendCommand(tabId, "Fetch.continueRequest", {
|
|
521
|
+
requestId: params.requestId
|
|
522
|
+
});
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function handleConsoleAPI(tabId, params) {
|
|
528
|
+
const messages = consoleMessages.get(tabId) || [];
|
|
529
|
+
if (messages.length >= MAX_CONSOLE_MESSAGES) {
|
|
530
|
+
messages.shift();
|
|
531
|
+
}
|
|
532
|
+
const text = params.args.map((arg) => arg.value !== void 0 ? String(arg.value) : arg.description || "").join(" ");
|
|
533
|
+
const typeMap = {
|
|
534
|
+
log: "log",
|
|
535
|
+
info: "info",
|
|
536
|
+
warning: "warn",
|
|
537
|
+
error: "error",
|
|
538
|
+
debug: "debug"
|
|
539
|
+
};
|
|
540
|
+
messages.push({
|
|
541
|
+
type: typeMap[params.type] || "log",
|
|
542
|
+
text,
|
|
543
|
+
timestamp: params.timestamp,
|
|
544
|
+
url: params.stackTrace?.callFrames[0]?.url,
|
|
545
|
+
lineNumber: params.stackTrace?.callFrames[0]?.lineNumber
|
|
546
|
+
});
|
|
547
|
+
consoleMessages.set(tabId, messages);
|
|
548
|
+
}
|
|
549
|
+
function handleLogEntry(tabId, params) {
|
|
550
|
+
const messages = consoleMessages.get(tabId) || [];
|
|
551
|
+
if (messages.length >= MAX_CONSOLE_MESSAGES) {
|
|
552
|
+
messages.shift();
|
|
553
|
+
}
|
|
554
|
+
const typeMap = {
|
|
555
|
+
verbose: "debug",
|
|
556
|
+
info: "info",
|
|
557
|
+
warning: "warn",
|
|
558
|
+
error: "error"
|
|
559
|
+
};
|
|
560
|
+
messages.push({
|
|
561
|
+
type: typeMap[params.entry.level] || "log",
|
|
562
|
+
text: params.entry.text,
|
|
563
|
+
timestamp: params.entry.timestamp,
|
|
564
|
+
url: params.entry.url,
|
|
565
|
+
lineNumber: params.entry.lineNumber
|
|
566
|
+
});
|
|
567
|
+
consoleMessages.set(tabId, messages);
|
|
568
|
+
}
|
|
569
|
+
function handleException(tabId, params) {
|
|
570
|
+
const errors = jsErrors.get(tabId) || [];
|
|
571
|
+
if (errors.length >= MAX_ERRORS) {
|
|
572
|
+
errors.shift();
|
|
573
|
+
}
|
|
574
|
+
const details = params.exceptionDetails;
|
|
575
|
+
const stackTrace = details.stackTrace?.callFrames.map((f) => ` at ${f.url}:${f.lineNumber}:${f.columnNumber}`).join("\n");
|
|
576
|
+
errors.push({
|
|
577
|
+
message: details.exception?.description || details.text,
|
|
578
|
+
url: details.url,
|
|
579
|
+
lineNumber: details.lineNumber,
|
|
580
|
+
columnNumber: details.columnNumber,
|
|
581
|
+
stackTrace,
|
|
582
|
+
timestamp: params.timestamp
|
|
583
|
+
});
|
|
584
|
+
jsErrors.set(tabId, errors);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function getSnapshot$1(tabId, options = {}) {
|
|
588
|
+
const { interactive = false } = options;
|
|
589
|
+
console.log("[DOMService] Getting snapshot for tab:", tabId, { interactive });
|
|
590
|
+
await injectBuildDomTreeScript(tabId);
|
|
591
|
+
const domTreeResult = await executeBuildDomTree(tabId);
|
|
592
|
+
const snapshotResult = interactive ? convertToAccessibilityTree(domTreeResult) : convertToFullTree(domTreeResult);
|
|
593
|
+
snapshotResult.refs;
|
|
594
|
+
console.log("[DOMService] Snapshot complete:", {
|
|
595
|
+
mode: interactive ? "interactive" : "full",
|
|
596
|
+
linesCount: snapshotResult.snapshot.split("\n").length,
|
|
597
|
+
refsCount: Object.keys(snapshotResult.refs).length
|
|
598
|
+
});
|
|
599
|
+
return snapshotResult;
|
|
600
|
+
}
|
|
601
|
+
function getFrameTarget(tabId) {
|
|
602
|
+
return { tabId };
|
|
603
|
+
}
|
|
604
|
+
async function injectBuildDomTreeScript(tabId) {
|
|
605
|
+
try {
|
|
606
|
+
const target = getFrameTarget(tabId);
|
|
607
|
+
const checkResults = await chrome.scripting.executeScript({
|
|
608
|
+
target,
|
|
609
|
+
func: () => Object.prototype.hasOwnProperty.call(window, "buildDomTree")
|
|
610
|
+
});
|
|
611
|
+
const isInjected = checkResults[0]?.result;
|
|
612
|
+
if (isInjected) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
await chrome.scripting.executeScript({
|
|
616
|
+
target,
|
|
617
|
+
files: ["buildDomTree.js"]
|
|
618
|
+
});
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.error("[DOMService] Failed to inject buildDomTree script:", error);
|
|
621
|
+
throw new Error(`Failed to inject script: ${error instanceof Error ? error.message : String(error)}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async function executeBuildDomTree(tabId) {
|
|
625
|
+
const target = getFrameTarget(tabId);
|
|
626
|
+
const results = await chrome.scripting.executeScript({
|
|
627
|
+
target,
|
|
628
|
+
func: (args) => {
|
|
629
|
+
return window.buildDomTree(args);
|
|
630
|
+
},
|
|
631
|
+
args: [{
|
|
632
|
+
showHighlightElements: true,
|
|
633
|
+
focusHighlightIndex: -1,
|
|
634
|
+
viewportExpansion: -1,
|
|
635
|
+
// -1 = 全页面模式,不限制视口
|
|
636
|
+
debugMode: false,
|
|
637
|
+
startId: 0,
|
|
638
|
+
startHighlightIndex: 0
|
|
639
|
+
}]
|
|
640
|
+
});
|
|
641
|
+
const result = results[0]?.result;
|
|
642
|
+
if (!result || !result.map || !result.rootId) {
|
|
643
|
+
throw new Error("Failed to build DOM tree: invalid result structure");
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
function getRole(node) {
|
|
648
|
+
const tagName = node.tagName.toLowerCase();
|
|
649
|
+
const role = node.attributes?.role;
|
|
650
|
+
if (role) return role;
|
|
651
|
+
const roleMap = {
|
|
652
|
+
a: "link",
|
|
653
|
+
button: "button",
|
|
654
|
+
input: getInputRole(node),
|
|
655
|
+
select: "combobox",
|
|
656
|
+
textarea: "textbox",
|
|
657
|
+
img: "image",
|
|
658
|
+
nav: "navigation",
|
|
659
|
+
main: "main",
|
|
660
|
+
header: "banner",
|
|
661
|
+
footer: "contentinfo",
|
|
662
|
+
aside: "complementary",
|
|
663
|
+
form: "form",
|
|
664
|
+
table: "table",
|
|
665
|
+
ul: "list",
|
|
666
|
+
ol: "list",
|
|
667
|
+
li: "listitem",
|
|
668
|
+
h1: "heading",
|
|
669
|
+
h2: "heading",
|
|
670
|
+
h3: "heading",
|
|
671
|
+
h4: "heading",
|
|
672
|
+
h5: "heading",
|
|
673
|
+
h6: "heading",
|
|
674
|
+
dialog: "dialog",
|
|
675
|
+
article: "article",
|
|
676
|
+
section: "region",
|
|
677
|
+
label: "label",
|
|
678
|
+
details: "group",
|
|
679
|
+
summary: "button"
|
|
680
|
+
};
|
|
681
|
+
return roleMap[tagName] || tagName;
|
|
682
|
+
}
|
|
683
|
+
function getInputRole(node) {
|
|
684
|
+
const type = node.attributes?.type?.toLowerCase() || "text";
|
|
685
|
+
const inputRoleMap = {
|
|
686
|
+
text: "textbox",
|
|
687
|
+
password: "textbox",
|
|
688
|
+
email: "textbox",
|
|
689
|
+
url: "textbox",
|
|
690
|
+
tel: "textbox",
|
|
691
|
+
search: "searchbox",
|
|
692
|
+
number: "spinbutton",
|
|
693
|
+
range: "slider",
|
|
694
|
+
checkbox: "checkbox",
|
|
695
|
+
radio: "radio",
|
|
696
|
+
button: "button",
|
|
697
|
+
submit: "button",
|
|
698
|
+
reset: "button",
|
|
699
|
+
file: "button"
|
|
700
|
+
};
|
|
701
|
+
return inputRoleMap[type] || "textbox";
|
|
702
|
+
}
|
|
703
|
+
function getAccessibleName(node, nodeMap) {
|
|
704
|
+
const attrs = node.attributes || {};
|
|
705
|
+
if (attrs["aria-label"]) return attrs["aria-label"];
|
|
706
|
+
if (attrs.title) return attrs.title;
|
|
707
|
+
if (attrs.placeholder) return attrs.placeholder;
|
|
708
|
+
if (attrs.alt) return attrs.alt;
|
|
709
|
+
if (attrs.value) return attrs.value;
|
|
710
|
+
const textContent = collectTextContent(node, nodeMap);
|
|
711
|
+
if (textContent) return textContent;
|
|
712
|
+
if (attrs.name) return attrs.name;
|
|
713
|
+
return void 0;
|
|
714
|
+
}
|
|
715
|
+
function collectTextContent(node, nodeMap, maxDepth = 5) {
|
|
716
|
+
const texts = [];
|
|
717
|
+
function collect(nodeId, depth) {
|
|
718
|
+
if (depth > maxDepth) return;
|
|
719
|
+
const currentNode = nodeMap[nodeId];
|
|
720
|
+
if (!currentNode) return;
|
|
721
|
+
if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
|
|
722
|
+
const text = currentNode.text.trim();
|
|
723
|
+
if (text) texts.push(text);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const elementNode = currentNode;
|
|
727
|
+
for (const childId of elementNode.children || []) {
|
|
728
|
+
collect(childId, depth + 1);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const childId of node.children || []) {
|
|
732
|
+
collect(childId, 0);
|
|
733
|
+
}
|
|
734
|
+
return texts.join(" ").trim();
|
|
735
|
+
}
|
|
736
|
+
function truncateText(text, maxLength = 50) {
|
|
737
|
+
if (text.length <= maxLength) return text;
|
|
738
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
739
|
+
}
|
|
740
|
+
function isAncestor(ancestorId, descendantId, nodeMap) {
|
|
741
|
+
const descendant = nodeMap[descendantId];
|
|
742
|
+
if (!descendant || "type" in descendant) return false;
|
|
743
|
+
const ancestor = nodeMap[ancestorId];
|
|
744
|
+
if (!ancestor || "type" in ancestor) return false;
|
|
745
|
+
const ancestorElement = ancestor;
|
|
746
|
+
const elementNode = descendant;
|
|
747
|
+
if (!ancestorElement.xpath || !elementNode.xpath) return false;
|
|
748
|
+
return elementNode.xpath.startsWith(ancestorElement.xpath + "/");
|
|
749
|
+
}
|
|
750
|
+
function convertToAccessibilityTree(result) {
|
|
751
|
+
const lines = [];
|
|
752
|
+
const refs = {};
|
|
753
|
+
const { map } = result;
|
|
754
|
+
const interactiveNodes = [];
|
|
755
|
+
for (const [id, node] of Object.entries(map)) {
|
|
756
|
+
if (!node) continue;
|
|
757
|
+
if ("type" in node && node.type === "TEXT_NODE") continue;
|
|
758
|
+
const elementNode = node;
|
|
759
|
+
if (elementNode.highlightIndex !== void 0 && elementNode.highlightIndex !== null) {
|
|
760
|
+
interactiveNodes.push({ id, node: elementNode });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
interactiveNodes.sort((a, b) => (a.node.highlightIndex ?? 0) - (b.node.highlightIndex ?? 0));
|
|
764
|
+
const nodeIdToInfo = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const { id, node } of interactiveNodes) {
|
|
766
|
+
nodeIdToInfo.set(id, {
|
|
767
|
+
name: getAccessibleName(node, map),
|
|
768
|
+
role: getRole(node)
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const nonSemanticTags = /* @__PURE__ */ new Set(["span", "div", "i", "b", "em", "strong", "small", "svg", "path", "g"]);
|
|
772
|
+
const filterableTags = /* @__PURE__ */ new Set(["label"]);
|
|
773
|
+
const filteredNodes = [];
|
|
774
|
+
for (const item of interactiveNodes) {
|
|
775
|
+
const { id, node } = item;
|
|
776
|
+
const info = nodeIdToInfo.get(id);
|
|
777
|
+
const tagName = node.tagName.toLowerCase();
|
|
778
|
+
if ((nonSemanticTags.has(tagName) || tagName === "a") && !info.name) continue;
|
|
779
|
+
if (filterableTags.has(tagName)) continue;
|
|
780
|
+
let isChildOfInteractive = false;
|
|
781
|
+
for (const otherItem of interactiveNodes) {
|
|
782
|
+
if (otherItem.id === id) continue;
|
|
783
|
+
const otherTagName = otherItem.node.tagName.toLowerCase();
|
|
784
|
+
if (["a", "button"].includes(otherTagName) && nonSemanticTags.has(tagName) && isAncestor(otherItem.id, id, map)) {
|
|
785
|
+
isChildOfInteractive = true;
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (isChildOfInteractive) continue;
|
|
790
|
+
let isDuplicate = false;
|
|
791
|
+
for (const otherItem of interactiveNodes) {
|
|
792
|
+
if (otherItem.id === id) continue;
|
|
793
|
+
const otherInfo = nodeIdToInfo.get(otherItem.id);
|
|
794
|
+
if (isAncestor(otherItem.id, id, map) && info.name && otherInfo.name && info.name === otherInfo.name) {
|
|
795
|
+
isDuplicate = true;
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!isDuplicate) filteredNodes.push(item);
|
|
800
|
+
}
|
|
801
|
+
for (const { id, node } of filteredNodes) {
|
|
802
|
+
const refId = String(node.highlightIndex);
|
|
803
|
+
const info = nodeIdToInfo.get(id);
|
|
804
|
+
let line = `- ${info.role}`;
|
|
805
|
+
if (info.name) line += ` "${truncateText(info.name)}"`;
|
|
806
|
+
line += ` [ref=${refId}]`;
|
|
807
|
+
lines.push(line);
|
|
808
|
+
refs[refId] = {
|
|
809
|
+
xpath: node.xpath || "",
|
|
810
|
+
role: info.role,
|
|
811
|
+
name: info.name ? truncateText(info.name, 100) : void 0,
|
|
812
|
+
tagName: node.tagName
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return { snapshot: lines.join("\n"), refs };
|
|
816
|
+
}
|
|
817
|
+
function convertToFullTree(result) {
|
|
818
|
+
const lines = [];
|
|
819
|
+
const refs = {};
|
|
820
|
+
const { rootId, map } = result;
|
|
821
|
+
const skipTags = /* @__PURE__ */ new Set([
|
|
822
|
+
"script",
|
|
823
|
+
"style",
|
|
824
|
+
"noscript",
|
|
825
|
+
"svg",
|
|
826
|
+
"path",
|
|
827
|
+
"g",
|
|
828
|
+
"defs",
|
|
829
|
+
"clippath",
|
|
830
|
+
"lineargradient",
|
|
831
|
+
"stop",
|
|
832
|
+
"symbol",
|
|
833
|
+
"use",
|
|
834
|
+
"meta",
|
|
835
|
+
"link",
|
|
836
|
+
"head"
|
|
837
|
+
]);
|
|
838
|
+
function traverse(nodeId, depth) {
|
|
839
|
+
const node = map[nodeId];
|
|
840
|
+
if (!node) return;
|
|
841
|
+
const indent = " ".repeat(depth);
|
|
842
|
+
if ("type" in node && node.type === "TEXT_NODE") {
|
|
843
|
+
if (!node.isVisible) return;
|
|
844
|
+
const text = node.text.trim();
|
|
845
|
+
if (!text) return;
|
|
846
|
+
const displayText = text.length > 100 ? text.slice(0, 97) + "..." : text;
|
|
847
|
+
lines.push(`${indent}- text: ${displayText}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const elementNode = node;
|
|
851
|
+
const tagName = elementNode.tagName.toLowerCase();
|
|
852
|
+
if (elementNode.isVisible === false) return;
|
|
853
|
+
if (skipTags.has(tagName)) return;
|
|
854
|
+
const role = getRole(elementNode);
|
|
855
|
+
const name = getAccessibleName(elementNode, map);
|
|
856
|
+
const hasRef = elementNode.highlightIndex !== void 0 && elementNode.highlightIndex !== null;
|
|
857
|
+
let line = `${indent}- ${role}`;
|
|
858
|
+
if (name) {
|
|
859
|
+
const displayName = name.length > 50 ? name.slice(0, 47) + "..." : name;
|
|
860
|
+
line += ` "${displayName}"`;
|
|
861
|
+
}
|
|
862
|
+
if (hasRef) {
|
|
863
|
+
const refId = String(elementNode.highlightIndex);
|
|
864
|
+
line += ` [ref=${refId}]`;
|
|
865
|
+
refs[refId] = {
|
|
866
|
+
xpath: elementNode.xpath || "",
|
|
867
|
+
role,
|
|
868
|
+
name: name ? name.length > 100 ? name.slice(0, 97) + "..." : name : void 0,
|
|
869
|
+
tagName: elementNode.tagName
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
lines.push(line);
|
|
873
|
+
for (const childId of elementNode.children || []) {
|
|
874
|
+
traverse(childId, depth + 1);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const rootNode = map[rootId];
|
|
878
|
+
if (rootNode && !("type" in rootNode)) {
|
|
879
|
+
for (const childId of rootNode.children || []) {
|
|
880
|
+
traverse(childId, 0);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return { snapshot: lines.join("\n"), refs };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
let lastSnapshotRefs = {};
|
|
887
|
+
async function loadRefsFromStorage() {
|
|
888
|
+
try {
|
|
889
|
+
const result = await chrome.storage.session.get("snapshotRefs");
|
|
890
|
+
if (result.snapshotRefs) {
|
|
891
|
+
lastSnapshotRefs = result.snapshotRefs;
|
|
892
|
+
console.log("[CDPDOMService] Loaded refs from storage:", Object.keys(lastSnapshotRefs).length);
|
|
893
|
+
}
|
|
894
|
+
} catch (e) {
|
|
895
|
+
console.warn("[CDPDOMService] Failed to load refs from storage:", e);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function saveRefsToStorage(refs) {
|
|
899
|
+
try {
|
|
900
|
+
await chrome.storage.session.set({ snapshotRefs: refs });
|
|
901
|
+
console.log("[CDPDOMService] Saved refs to storage:", Object.keys(refs).length);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
console.warn("[CDPDOMService] Failed to save refs to storage:", e);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
loadRefsFromStorage();
|
|
907
|
+
async function getSnapshot(tabId, options = {}) {
|
|
908
|
+
console.log("[CDPDOMService] Getting snapshot via legacy method for tab:", tabId);
|
|
909
|
+
const result = await getSnapshot$1(tabId, options);
|
|
910
|
+
const convertedRefs = {};
|
|
911
|
+
for (const [refId, refInfo] of Object.entries(result.refs)) {
|
|
912
|
+
convertedRefs[refId] = {
|
|
913
|
+
xpath: refInfo.xpath,
|
|
914
|
+
role: refInfo.role,
|
|
915
|
+
name: refInfo.name,
|
|
916
|
+
tagName: refInfo.tagName
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
lastSnapshotRefs = convertedRefs;
|
|
920
|
+
await saveRefsToStorage(convertedRefs);
|
|
921
|
+
console.log("[CDPDOMService] Snapshot complete:", {
|
|
922
|
+
linesCount: result.snapshot.split("\n").length,
|
|
923
|
+
refsCount: Object.keys(convertedRefs).length
|
|
924
|
+
});
|
|
925
|
+
return {
|
|
926
|
+
snapshot: result.snapshot,
|
|
927
|
+
refs: convertedRefs
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
async function getRefInfo(ref) {
|
|
931
|
+
const refId = ref.startsWith("@") ? ref.slice(1) : ref;
|
|
932
|
+
if (lastSnapshotRefs[refId]) {
|
|
933
|
+
return lastSnapshotRefs[refId];
|
|
934
|
+
}
|
|
935
|
+
if (Object.keys(lastSnapshotRefs).length === 0) {
|
|
936
|
+
await loadRefsFromStorage();
|
|
937
|
+
}
|
|
938
|
+
return lastSnapshotRefs[refId] || null;
|
|
939
|
+
}
|
|
940
|
+
async function getElementCenterByXPath(tabId, xpath) {
|
|
941
|
+
const result = await evaluate(tabId, `
|
|
942
|
+
(function() {
|
|
943
|
+
const result = document.evaluate(
|
|
944
|
+
${JSON.stringify(xpath)},
|
|
945
|
+
document,
|
|
946
|
+
null,
|
|
947
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
948
|
+
null
|
|
949
|
+
);
|
|
950
|
+
const element = result.singleNodeValue;
|
|
951
|
+
if (!element) return null;
|
|
952
|
+
|
|
953
|
+
// 滚动到可见
|
|
954
|
+
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
955
|
+
|
|
956
|
+
const rect = element.getBoundingClientRect();
|
|
957
|
+
return {
|
|
958
|
+
x: rect.left + rect.width / 2,
|
|
959
|
+
y: rect.top + rect.height / 2,
|
|
960
|
+
};
|
|
961
|
+
})()
|
|
962
|
+
`, { returnByValue: true });
|
|
963
|
+
if (!result) {
|
|
964
|
+
throw new Error(`Element not found by xpath: ${xpath}`);
|
|
965
|
+
}
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
async function clickElement(tabId, ref) {
|
|
969
|
+
const refInfo = await getRefInfo(ref);
|
|
970
|
+
if (!refInfo) {
|
|
971
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
972
|
+
}
|
|
973
|
+
const { xpath, role, name } = refInfo;
|
|
974
|
+
const { x, y } = await getElementCenterByXPath(tabId, xpath);
|
|
975
|
+
await click(tabId, x, y);
|
|
976
|
+
console.log("[CDPDOMService] Clicked element:", { ref, role, name, x, y });
|
|
977
|
+
return { role, name };
|
|
978
|
+
}
|
|
979
|
+
async function hoverElement(tabId, ref) {
|
|
980
|
+
const refInfo = await getRefInfo(ref);
|
|
981
|
+
if (!refInfo) {
|
|
982
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
983
|
+
}
|
|
984
|
+
const { xpath, role, name } = refInfo;
|
|
985
|
+
const { x, y } = await getElementCenterByXPath(tabId, xpath);
|
|
986
|
+
await moveMouse(tabId, x, y);
|
|
987
|
+
console.log("[CDPDOMService] Hovered element:", { ref, role, name, x, y });
|
|
988
|
+
return { role, name };
|
|
989
|
+
}
|
|
990
|
+
async function fillElement(tabId, ref, text) {
|
|
991
|
+
const refInfo = await getRefInfo(ref);
|
|
992
|
+
if (!refInfo) {
|
|
993
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
994
|
+
}
|
|
995
|
+
const { xpath, role, name } = refInfo;
|
|
996
|
+
await evaluate(tabId, `
|
|
997
|
+
(function() {
|
|
998
|
+
const result = document.evaluate(
|
|
999
|
+
${JSON.stringify(xpath)},
|
|
1000
|
+
document,
|
|
1001
|
+
null,
|
|
1002
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1003
|
+
null
|
|
1004
|
+
);
|
|
1005
|
+
const element = result.singleNodeValue;
|
|
1006
|
+
if (!element) throw new Error('Element not found');
|
|
1007
|
+
|
|
1008
|
+
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1009
|
+
element.focus();
|
|
1010
|
+
|
|
1011
|
+
// 清空内容
|
|
1012
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
1013
|
+
element.value = '';
|
|
1014
|
+
} else if (element.isContentEditable) {
|
|
1015
|
+
element.textContent = '';
|
|
1016
|
+
}
|
|
1017
|
+
})()
|
|
1018
|
+
`);
|
|
1019
|
+
await insertText(tabId, text);
|
|
1020
|
+
console.log("[CDPDOMService] Filled element:", { ref, role, name, textLength: text.length });
|
|
1021
|
+
return { role, name };
|
|
1022
|
+
}
|
|
1023
|
+
async function typeElement(tabId, ref, text) {
|
|
1024
|
+
const refInfo = await getRefInfo(ref);
|
|
1025
|
+
if (!refInfo) {
|
|
1026
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1027
|
+
}
|
|
1028
|
+
const { xpath, role, name } = refInfo;
|
|
1029
|
+
await evaluate(tabId, `
|
|
1030
|
+
(function() {
|
|
1031
|
+
const result = document.evaluate(
|
|
1032
|
+
${JSON.stringify(xpath)},
|
|
1033
|
+
document,
|
|
1034
|
+
null,
|
|
1035
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1036
|
+
null
|
|
1037
|
+
);
|
|
1038
|
+
const element = result.singleNodeValue;
|
|
1039
|
+
if (!element) throw new Error('Element not found');
|
|
1040
|
+
|
|
1041
|
+
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
1042
|
+
element.focus();
|
|
1043
|
+
})()
|
|
1044
|
+
`);
|
|
1045
|
+
for (const char of text) {
|
|
1046
|
+
await pressKey$1(tabId, char);
|
|
1047
|
+
}
|
|
1048
|
+
console.log("[CDPDOMService] Typed in element:", { ref, role, name, textLength: text.length });
|
|
1049
|
+
return { role, name };
|
|
1050
|
+
}
|
|
1051
|
+
async function getElementText(tabId, ref) {
|
|
1052
|
+
const refInfo = await getRefInfo(ref);
|
|
1053
|
+
if (!refInfo) {
|
|
1054
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1055
|
+
}
|
|
1056
|
+
const { xpath } = refInfo;
|
|
1057
|
+
const text = await evaluate(tabId, `
|
|
1058
|
+
(function() {
|
|
1059
|
+
const result = document.evaluate(
|
|
1060
|
+
${JSON.stringify(xpath)},
|
|
1061
|
+
document,
|
|
1062
|
+
null,
|
|
1063
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1064
|
+
null
|
|
1065
|
+
);
|
|
1066
|
+
const element = result.singleNodeValue;
|
|
1067
|
+
if (!element) return '';
|
|
1068
|
+
return (element.textContent || '').trim();
|
|
1069
|
+
})()
|
|
1070
|
+
`);
|
|
1071
|
+
console.log("[CDPDOMService] Got element text:", { ref, textLength: text.length });
|
|
1072
|
+
return text;
|
|
1073
|
+
}
|
|
1074
|
+
async function checkElement(tabId, ref) {
|
|
1075
|
+
const refInfo = await getRefInfo(ref);
|
|
1076
|
+
if (!refInfo) {
|
|
1077
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1078
|
+
}
|
|
1079
|
+
const { xpath, role, name } = refInfo;
|
|
1080
|
+
const result = await evaluate(tabId, `
|
|
1081
|
+
(function() {
|
|
1082
|
+
const result = document.evaluate(
|
|
1083
|
+
${JSON.stringify(xpath)},
|
|
1084
|
+
document,
|
|
1085
|
+
null,
|
|
1086
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1087
|
+
null
|
|
1088
|
+
);
|
|
1089
|
+
const element = result.singleNodeValue;
|
|
1090
|
+
if (!element) throw new Error('Element not found');
|
|
1091
|
+
if (element.type !== 'checkbox' && element.type !== 'radio') {
|
|
1092
|
+
throw new Error('Element is not a checkbox or radio');
|
|
1093
|
+
}
|
|
1094
|
+
const wasChecked = element.checked;
|
|
1095
|
+
if (!wasChecked) {
|
|
1096
|
+
element.checked = true;
|
|
1097
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1098
|
+
}
|
|
1099
|
+
return wasChecked;
|
|
1100
|
+
})()
|
|
1101
|
+
`);
|
|
1102
|
+
console.log("[CDPDOMService] Checked element:", { ref, role, name, wasAlreadyChecked: result });
|
|
1103
|
+
return { role, name, wasAlreadyChecked: result };
|
|
1104
|
+
}
|
|
1105
|
+
async function uncheckElement(tabId, ref) {
|
|
1106
|
+
const refInfo = await getRefInfo(ref);
|
|
1107
|
+
if (!refInfo) {
|
|
1108
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1109
|
+
}
|
|
1110
|
+
const { xpath, role, name } = refInfo;
|
|
1111
|
+
const result = await evaluate(tabId, `
|
|
1112
|
+
(function() {
|
|
1113
|
+
const result = document.evaluate(
|
|
1114
|
+
${JSON.stringify(xpath)},
|
|
1115
|
+
document,
|
|
1116
|
+
null,
|
|
1117
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1118
|
+
null
|
|
1119
|
+
);
|
|
1120
|
+
const element = result.singleNodeValue;
|
|
1121
|
+
if (!element) throw new Error('Element not found');
|
|
1122
|
+
if (element.type !== 'checkbox' && element.type !== 'radio') {
|
|
1123
|
+
throw new Error('Element is not a checkbox or radio');
|
|
1124
|
+
}
|
|
1125
|
+
const wasUnchecked = !element.checked;
|
|
1126
|
+
if (!wasUnchecked) {
|
|
1127
|
+
element.checked = false;
|
|
1128
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1129
|
+
}
|
|
1130
|
+
return wasUnchecked;
|
|
1131
|
+
})()
|
|
1132
|
+
`);
|
|
1133
|
+
console.log("[CDPDOMService] Unchecked element:", { ref, role, name, wasAlreadyUnchecked: result });
|
|
1134
|
+
return { role, name, wasAlreadyUnchecked: result };
|
|
1135
|
+
}
|
|
1136
|
+
async function selectOption(tabId, ref, value) {
|
|
1137
|
+
const refInfo = await getRefInfo(ref);
|
|
1138
|
+
if (!refInfo) {
|
|
1139
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1140
|
+
}
|
|
1141
|
+
const { xpath, role, name } = refInfo;
|
|
1142
|
+
const result = await evaluate(tabId, `
|
|
1143
|
+
(function() {
|
|
1144
|
+
const selectValue = ${JSON.stringify(value)};
|
|
1145
|
+
const result = document.evaluate(
|
|
1146
|
+
${JSON.stringify(xpath)},
|
|
1147
|
+
document,
|
|
1148
|
+
null,
|
|
1149
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1150
|
+
null
|
|
1151
|
+
);
|
|
1152
|
+
const element = result.singleNodeValue;
|
|
1153
|
+
if (!element) throw new Error('Element not found');
|
|
1154
|
+
if (element.tagName !== 'SELECT') {
|
|
1155
|
+
throw new Error('Element is not a <select> element');
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// 尝试通过 value 或 text 匹配
|
|
1159
|
+
let matched = null;
|
|
1160
|
+
for (const opt of element.options) {
|
|
1161
|
+
if (opt.value === selectValue || opt.textContent.trim() === selectValue) {
|
|
1162
|
+
matched = opt;
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// 不区分大小写匹配
|
|
1168
|
+
if (!matched) {
|
|
1169
|
+
const lower = selectValue.toLowerCase();
|
|
1170
|
+
for (const opt of element.options) {
|
|
1171
|
+
if (opt.value.toLowerCase() === lower || opt.textContent.trim().toLowerCase() === lower) {
|
|
1172
|
+
matched = opt;
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (!matched) {
|
|
1179
|
+
const available = Array.from(element.options).map(o => ({ value: o.value, label: o.textContent.trim() }));
|
|
1180
|
+
throw new Error('Option not found: ' + selectValue + '. Available: ' + JSON.stringify(available));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
element.value = matched.value;
|
|
1184
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1185
|
+
|
|
1186
|
+
return { selectedValue: matched.value, selectedLabel: matched.textContent.trim() };
|
|
1187
|
+
})()
|
|
1188
|
+
`);
|
|
1189
|
+
const { selectedValue, selectedLabel } = result;
|
|
1190
|
+
console.log("[CDPDOMService] Selected option:", { ref, role, name, selectedValue });
|
|
1191
|
+
return { role, name, selectedValue, selectedLabel };
|
|
1192
|
+
}
|
|
1193
|
+
async function waitForElement(tabId, ref, maxWait = 1e4, interval = 200) {
|
|
1194
|
+
const refInfo = await getRefInfo(ref);
|
|
1195
|
+
if (!refInfo) {
|
|
1196
|
+
throw new Error(`Ref "${ref}" not found. Run snapshot first to get available refs.`);
|
|
1197
|
+
}
|
|
1198
|
+
const { xpath } = refInfo;
|
|
1199
|
+
let elapsed = 0;
|
|
1200
|
+
while (elapsed < maxWait) {
|
|
1201
|
+
const found = await evaluate(tabId, `
|
|
1202
|
+
(function() {
|
|
1203
|
+
const result = document.evaluate(
|
|
1204
|
+
${JSON.stringify(xpath)},
|
|
1205
|
+
document,
|
|
1206
|
+
null,
|
|
1207
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
1208
|
+
null
|
|
1209
|
+
);
|
|
1210
|
+
return result.singleNodeValue !== null;
|
|
1211
|
+
})()
|
|
1212
|
+
`);
|
|
1213
|
+
if (found) {
|
|
1214
|
+
console.log("[CDPDOMService] Element found:", { ref, elapsed });
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
1218
|
+
elapsed += interval;
|
|
1219
|
+
}
|
|
1220
|
+
throw new Error(`Timeout waiting for element @${ref} after ${maxWait}ms`);
|
|
1221
|
+
}
|
|
1222
|
+
function setActiveFrameId(frameId) {
|
|
1223
|
+
console.log("[CDPDOMService] Active frame changed:", frameId ?? "main");
|
|
1224
|
+
}
|
|
1225
|
+
async function pressKey(tabId, key, modifiers = []) {
|
|
1226
|
+
let modifierFlags = 0;
|
|
1227
|
+
if (modifiers.includes("Alt")) modifierFlags |= 1;
|
|
1228
|
+
if (modifiers.includes("Control")) modifierFlags |= 2;
|
|
1229
|
+
if (modifiers.includes("Meta")) modifierFlags |= 4;
|
|
1230
|
+
if (modifiers.includes("Shift")) modifierFlags |= 8;
|
|
1231
|
+
await pressKey$1(tabId, key, { modifiers: modifierFlags });
|
|
1232
|
+
console.log("[CDPDOMService] Pressed key:", key, modifiers);
|
|
1233
|
+
}
|
|
1234
|
+
async function scrollPage(tabId, direction, pixels) {
|
|
1235
|
+
const result = await evaluate(
|
|
1236
|
+
tabId,
|
|
1237
|
+
"JSON.stringify({ width: window.innerWidth, height: window.innerHeight })"
|
|
1238
|
+
);
|
|
1239
|
+
const { width, height } = JSON.parse(result);
|
|
1240
|
+
const x = width / 2;
|
|
1241
|
+
const y = height / 2;
|
|
1242
|
+
let deltaX = 0;
|
|
1243
|
+
let deltaY = 0;
|
|
1244
|
+
switch (direction) {
|
|
1245
|
+
case "up":
|
|
1246
|
+
deltaY = -pixels;
|
|
1247
|
+
break;
|
|
1248
|
+
case "down":
|
|
1249
|
+
deltaY = pixels;
|
|
1250
|
+
break;
|
|
1251
|
+
case "left":
|
|
1252
|
+
deltaX = -pixels;
|
|
1253
|
+
break;
|
|
1254
|
+
case "right":
|
|
1255
|
+
deltaX = pixels;
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
await scroll(tabId, x, y, deltaX, deltaY);
|
|
1259
|
+
console.log("[CDPDOMService] Scrolled:", { direction, pixels });
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
let isRecording = false;
|
|
1263
|
+
let recordingTabId = null;
|
|
1264
|
+
let events = [];
|
|
1265
|
+
async function startRecording(tabId) {
|
|
1266
|
+
console.log("[TraceService] Starting recording on tab:", tabId);
|
|
1267
|
+
isRecording = true;
|
|
1268
|
+
recordingTabId = tabId;
|
|
1269
|
+
events = [];
|
|
1270
|
+
try {
|
|
1271
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1272
|
+
if (tab.url) {
|
|
1273
|
+
events.push({
|
|
1274
|
+
type: "navigation",
|
|
1275
|
+
timestamp: Date.now(),
|
|
1276
|
+
url: tab.url,
|
|
1277
|
+
elementRole: "document",
|
|
1278
|
+
elementName: tab.title || "",
|
|
1279
|
+
elementTag: "document"
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
console.error("[TraceService] Error getting tab info:", error);
|
|
1284
|
+
}
|
|
1285
|
+
try {
|
|
1286
|
+
await chrome.tabs.sendMessage(tabId, { type: "TRACE_START" });
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
console.log("[TraceService] Content script not ready, will record on next event");
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
async function stopRecording() {
|
|
1292
|
+
console.log("[TraceService] Stopping recording, events:", events.length);
|
|
1293
|
+
const recordedEvents = [...events];
|
|
1294
|
+
if (recordingTabId !== null) {
|
|
1295
|
+
try {
|
|
1296
|
+
await chrome.tabs.sendMessage(recordingTabId, { type: "TRACE_STOP" });
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
console.log("[TraceService] Could not notify content script:", error);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
isRecording = false;
|
|
1302
|
+
recordingTabId = null;
|
|
1303
|
+
events = [];
|
|
1304
|
+
return recordedEvents;
|
|
1305
|
+
}
|
|
1306
|
+
function getStatus() {
|
|
1307
|
+
return {
|
|
1308
|
+
recording: isRecording,
|
|
1309
|
+
eventCount: events.length,
|
|
1310
|
+
tabId: recordingTabId ?? void 0
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function addEvent(event) {
|
|
1314
|
+
if (!isRecording) return;
|
|
1315
|
+
console.log("[TraceService] Adding event:", event.type, event);
|
|
1316
|
+
events.push(event);
|
|
1317
|
+
}
|
|
1318
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1319
|
+
if (message.type === "TRACE_EVENT") {
|
|
1320
|
+
if (isRecording && sender.tab?.id === recordingTabId) {
|
|
1321
|
+
addEvent(message.payload);
|
|
1322
|
+
}
|
|
1323
|
+
sendResponse({ received: true });
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
if (message.type === "GET_TRACE_STATUS") {
|
|
1327
|
+
sendResponse({
|
|
1328
|
+
recording: isRecording && sender.tab?.id === recordingTabId,
|
|
1329
|
+
tabId: recordingTabId
|
|
1330
|
+
});
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
return false;
|
|
1334
|
+
});
|
|
1335
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1336
|
+
if (tabId === recordingTabId) {
|
|
1337
|
+
console.log("[TraceService] Recording tab closed, stopping recording");
|
|
1338
|
+
isRecording = false;
|
|
1339
|
+
recordingTabId = null;
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
|
|
1343
|
+
if (tabId === recordingTabId && isRecording) {
|
|
1344
|
+
if (changeInfo.url) {
|
|
1345
|
+
events.push({
|
|
1346
|
+
type: "navigation",
|
|
1347
|
+
timestamp: Date.now(),
|
|
1348
|
+
url: changeInfo.url,
|
|
1349
|
+
elementRole: "document",
|
|
1350
|
+
elementName: _tab.title || "",
|
|
1351
|
+
elementTag: "document"
|
|
1352
|
+
});
|
|
1353
|
+
console.log("[TraceService] Navigation event:", changeInfo.url);
|
|
1354
|
+
}
|
|
1355
|
+
if (changeInfo.status === "complete") {
|
|
1356
|
+
console.log("[TraceService] Page loaded, notifying content script to start recording");
|
|
1357
|
+
try {
|
|
1358
|
+
await chrome.tabs.sendMessage(tabId, { type: "TRACE_START" });
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
console.log("[TraceService] Could not notify content script:", error);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
console.log("[TraceService] Initialized");
|
|
1366
|
+
|
|
1367
|
+
initEventListeners();
|
|
1368
|
+
let activeFrameId = null;
|
|
1369
|
+
async function handleCommand(command) {
|
|
1370
|
+
console.log("[CommandHandler] Processing command:", command.id, command.action);
|
|
1371
|
+
let result;
|
|
1372
|
+
try {
|
|
1373
|
+
switch (command.action) {
|
|
1374
|
+
case "open":
|
|
1375
|
+
result = await handleOpen(command);
|
|
1376
|
+
break;
|
|
1377
|
+
case "snapshot":
|
|
1378
|
+
result = await handleSnapshot(command);
|
|
1379
|
+
break;
|
|
1380
|
+
case "click":
|
|
1381
|
+
result = await handleClick(command);
|
|
1382
|
+
break;
|
|
1383
|
+
case "hover":
|
|
1384
|
+
result = await handleHover(command);
|
|
1385
|
+
break;
|
|
1386
|
+
case "fill":
|
|
1387
|
+
result = await handleFill(command);
|
|
1388
|
+
break;
|
|
1389
|
+
case "type":
|
|
1390
|
+
result = await handleType(command);
|
|
1391
|
+
break;
|
|
1392
|
+
case "check":
|
|
1393
|
+
result = await handleCheck(command);
|
|
1394
|
+
break;
|
|
1395
|
+
case "uncheck":
|
|
1396
|
+
result = await handleUncheck(command);
|
|
1397
|
+
break;
|
|
1398
|
+
case "close":
|
|
1399
|
+
result = await handleClose(command);
|
|
1400
|
+
break;
|
|
1401
|
+
case "get":
|
|
1402
|
+
result = await handleGet(command);
|
|
1403
|
+
break;
|
|
1404
|
+
case "screenshot":
|
|
1405
|
+
result = await handleScreenshot(command);
|
|
1406
|
+
break;
|
|
1407
|
+
case "wait":
|
|
1408
|
+
result = await handleWait(command);
|
|
1409
|
+
break;
|
|
1410
|
+
case "press":
|
|
1411
|
+
result = await handlePress(command);
|
|
1412
|
+
break;
|
|
1413
|
+
case "scroll":
|
|
1414
|
+
result = await handleScroll(command);
|
|
1415
|
+
break;
|
|
1416
|
+
case "back":
|
|
1417
|
+
result = await handleBack(command);
|
|
1418
|
+
break;
|
|
1419
|
+
case "forward":
|
|
1420
|
+
result = await handleForward(command);
|
|
1421
|
+
break;
|
|
1422
|
+
case "refresh":
|
|
1423
|
+
result = await handleRefresh(command);
|
|
1424
|
+
break;
|
|
1425
|
+
case "eval":
|
|
1426
|
+
result = await handleEval(command);
|
|
1427
|
+
break;
|
|
1428
|
+
case "select":
|
|
1429
|
+
result = await handleSelect(command);
|
|
1430
|
+
break;
|
|
1431
|
+
case "tab_list":
|
|
1432
|
+
result = await handleTabList(command);
|
|
1433
|
+
break;
|
|
1434
|
+
case "tab_new":
|
|
1435
|
+
result = await handleTabNew(command);
|
|
1436
|
+
break;
|
|
1437
|
+
case "tab_select":
|
|
1438
|
+
result = await handleTabSelect(command);
|
|
1439
|
+
break;
|
|
1440
|
+
case "tab_close":
|
|
1441
|
+
result = await handleTabClose(command);
|
|
1442
|
+
break;
|
|
1443
|
+
case "frame":
|
|
1444
|
+
result = await handleFrame(command);
|
|
1445
|
+
break;
|
|
1446
|
+
case "frame_main":
|
|
1447
|
+
result = await handleFrameMain(command);
|
|
1448
|
+
break;
|
|
1449
|
+
case "dialog":
|
|
1450
|
+
result = await handleDialog(command);
|
|
1451
|
+
break;
|
|
1452
|
+
case "network":
|
|
1453
|
+
result = await handleNetwork(command);
|
|
1454
|
+
break;
|
|
1455
|
+
case "console":
|
|
1456
|
+
result = await handleConsole(command);
|
|
1457
|
+
break;
|
|
1458
|
+
case "errors":
|
|
1459
|
+
result = await handleErrors(command);
|
|
1460
|
+
break;
|
|
1461
|
+
case "trace":
|
|
1462
|
+
result = await handleTrace(command);
|
|
1463
|
+
break;
|
|
1464
|
+
default:
|
|
1465
|
+
result = {
|
|
1466
|
+
id: command.id,
|
|
1467
|
+
success: false,
|
|
1468
|
+
error: `Unknown action: ${command.action}`
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
result = {
|
|
1473
|
+
id: command.id,
|
|
1474
|
+
success: false,
|
|
1475
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
await sendResult(result);
|
|
1479
|
+
}
|
|
1480
|
+
async function handleOpen(command) {
|
|
1481
|
+
const url = command.url;
|
|
1482
|
+
const tabIdParam = command.tabId;
|
|
1483
|
+
if (!url) {
|
|
1484
|
+
return {
|
|
1485
|
+
id: command.id,
|
|
1486
|
+
success: false,
|
|
1487
|
+
error: "Missing url parameter"
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
console.log("[CommandHandler] Opening URL:", url, "tabId:", tabIdParam);
|
|
1491
|
+
let tab;
|
|
1492
|
+
if (tabIdParam === void 0) {
|
|
1493
|
+
tab = await chrome.tabs.create({ url, active: true });
|
|
1494
|
+
} else if (tabIdParam === "current") {
|
|
1495
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1496
|
+
if (activeTab && activeTab.id) {
|
|
1497
|
+
tab = await chrome.tabs.update(activeTab.id, { url });
|
|
1498
|
+
} else {
|
|
1499
|
+
tab = await chrome.tabs.create({ url, active: true });
|
|
1500
|
+
}
|
|
1501
|
+
} else {
|
|
1502
|
+
const targetTabId = typeof tabIdParam === "number" ? tabIdParam : parseInt(String(tabIdParam), 10);
|
|
1503
|
+
if (isNaN(targetTabId)) {
|
|
1504
|
+
return {
|
|
1505
|
+
id: command.id,
|
|
1506
|
+
success: false,
|
|
1507
|
+
error: `Invalid tabId: ${tabIdParam}`
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
tab = await chrome.tabs.update(targetTabId, { url, active: true });
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
return {
|
|
1514
|
+
id: command.id,
|
|
1515
|
+
success: false,
|
|
1516
|
+
error: `Tab ${targetTabId} not found or cannot be updated`
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
await waitForTabLoad(tab.id);
|
|
1521
|
+
const updatedTab = await chrome.tabs.get(tab.id);
|
|
1522
|
+
return {
|
|
1523
|
+
id: command.id,
|
|
1524
|
+
success: true,
|
|
1525
|
+
data: {
|
|
1526
|
+
tabId: tab.id,
|
|
1527
|
+
title: updatedTab.title || "",
|
|
1528
|
+
url: updatedTab.url || url
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
async function handleSnapshot(command) {
|
|
1533
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1534
|
+
if (!activeTab || !activeTab.id) {
|
|
1535
|
+
return {
|
|
1536
|
+
id: command.id,
|
|
1537
|
+
success: false,
|
|
1538
|
+
error: "No active tab found"
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
const url = activeTab.url || "";
|
|
1542
|
+
if (url.startsWith("chrome://") || url.startsWith("about:") || url.startsWith("chrome-extension://")) {
|
|
1543
|
+
return {
|
|
1544
|
+
id: command.id,
|
|
1545
|
+
success: false,
|
|
1546
|
+
error: `Cannot take snapshot of restricted page: ${url}`
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
const interactive = command.interactive;
|
|
1550
|
+
console.log("[CommandHandler] Taking snapshot of tab:", activeTab.id, activeTab.url, { interactive });
|
|
1551
|
+
try {
|
|
1552
|
+
const snapshotResult = await getSnapshot(activeTab.id, { interactive });
|
|
1553
|
+
return {
|
|
1554
|
+
id: command.id,
|
|
1555
|
+
success: true,
|
|
1556
|
+
data: {
|
|
1557
|
+
title: activeTab.title || "",
|
|
1558
|
+
url: activeTab.url || "",
|
|
1559
|
+
snapshotData: snapshotResult
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
console.error("[CommandHandler] Snapshot failed:", error);
|
|
1564
|
+
return {
|
|
1565
|
+
id: command.id,
|
|
1566
|
+
success: false,
|
|
1567
|
+
error: `Snapshot failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
async function handleClick(command) {
|
|
1572
|
+
const ref = command.ref;
|
|
1573
|
+
if (!ref) {
|
|
1574
|
+
return {
|
|
1575
|
+
id: command.id,
|
|
1576
|
+
success: false,
|
|
1577
|
+
error: "Missing ref parameter"
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1581
|
+
if (!activeTab || !activeTab.id) {
|
|
1582
|
+
return {
|
|
1583
|
+
id: command.id,
|
|
1584
|
+
success: false,
|
|
1585
|
+
error: "No active tab found"
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
console.log("[CommandHandler] Clicking element:", ref);
|
|
1589
|
+
try {
|
|
1590
|
+
const elementInfo = await clickElement(activeTab.id, ref);
|
|
1591
|
+
return {
|
|
1592
|
+
id: command.id,
|
|
1593
|
+
success: true,
|
|
1594
|
+
data: {
|
|
1595
|
+
role: elementInfo.role,
|
|
1596
|
+
name: elementInfo.name
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
console.error("[CommandHandler] Click failed:", error);
|
|
1601
|
+
return {
|
|
1602
|
+
id: command.id,
|
|
1603
|
+
success: false,
|
|
1604
|
+
error: `Click failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
async function handleHover(command) {
|
|
1609
|
+
const ref = command.ref;
|
|
1610
|
+
if (!ref) {
|
|
1611
|
+
return {
|
|
1612
|
+
id: command.id,
|
|
1613
|
+
success: false,
|
|
1614
|
+
error: "Missing ref parameter"
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1618
|
+
if (!activeTab || !activeTab.id) {
|
|
1619
|
+
return {
|
|
1620
|
+
id: command.id,
|
|
1621
|
+
success: false,
|
|
1622
|
+
error: "No active tab found"
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
console.log("[CommandHandler] Hovering element:", ref);
|
|
1626
|
+
try {
|
|
1627
|
+
const elementInfo = await hoverElement(activeTab.id, ref);
|
|
1628
|
+
return {
|
|
1629
|
+
id: command.id,
|
|
1630
|
+
success: true,
|
|
1631
|
+
data: {
|
|
1632
|
+
role: elementInfo.role,
|
|
1633
|
+
name: elementInfo.name
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
console.error("[CommandHandler] Hover failed:", error);
|
|
1638
|
+
return {
|
|
1639
|
+
id: command.id,
|
|
1640
|
+
success: false,
|
|
1641
|
+
error: `Hover failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
async function handleFill(command) {
|
|
1646
|
+
const ref = command.ref;
|
|
1647
|
+
const text = command.text;
|
|
1648
|
+
if (!ref) {
|
|
1649
|
+
return {
|
|
1650
|
+
id: command.id,
|
|
1651
|
+
success: false,
|
|
1652
|
+
error: "Missing ref parameter"
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
if (text === void 0 || text === null) {
|
|
1656
|
+
return {
|
|
1657
|
+
id: command.id,
|
|
1658
|
+
success: false,
|
|
1659
|
+
error: "Missing text parameter"
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1663
|
+
if (!activeTab || !activeTab.id) {
|
|
1664
|
+
return {
|
|
1665
|
+
id: command.id,
|
|
1666
|
+
success: false,
|
|
1667
|
+
error: "No active tab found"
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
console.log("[CommandHandler] Filling element:", ref, "with text length:", text.length);
|
|
1671
|
+
try {
|
|
1672
|
+
const elementInfo = await fillElement(activeTab.id, ref, text);
|
|
1673
|
+
return {
|
|
1674
|
+
id: command.id,
|
|
1675
|
+
success: true,
|
|
1676
|
+
data: {
|
|
1677
|
+
role: elementInfo.role,
|
|
1678
|
+
name: elementInfo.name,
|
|
1679
|
+
filledText: text
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
console.error("[CommandHandler] Fill failed:", error);
|
|
1684
|
+
return {
|
|
1685
|
+
id: command.id,
|
|
1686
|
+
success: false,
|
|
1687
|
+
error: `Fill failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
async function handleType(command) {
|
|
1692
|
+
const ref = command.ref;
|
|
1693
|
+
const text = command.text;
|
|
1694
|
+
if (!ref) {
|
|
1695
|
+
return {
|
|
1696
|
+
id: command.id,
|
|
1697
|
+
success: false,
|
|
1698
|
+
error: "Missing ref parameter"
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
if (text === void 0 || text === null) {
|
|
1702
|
+
return {
|
|
1703
|
+
id: command.id,
|
|
1704
|
+
success: false,
|
|
1705
|
+
error: "Missing text parameter"
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1709
|
+
if (!activeTab || !activeTab.id) {
|
|
1710
|
+
return {
|
|
1711
|
+
id: command.id,
|
|
1712
|
+
success: false,
|
|
1713
|
+
error: "No active tab found"
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
console.log("[CommandHandler] Typing in element:", ref, "text length:", text.length);
|
|
1717
|
+
try {
|
|
1718
|
+
const elementInfo = await typeElement(activeTab.id, ref, text);
|
|
1719
|
+
return {
|
|
1720
|
+
id: command.id,
|
|
1721
|
+
success: true,
|
|
1722
|
+
data: {
|
|
1723
|
+
role: elementInfo.role,
|
|
1724
|
+
name: elementInfo.name,
|
|
1725
|
+
typedText: text
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
console.error("[CommandHandler] Type failed:", error);
|
|
1730
|
+
return {
|
|
1731
|
+
id: command.id,
|
|
1732
|
+
success: false,
|
|
1733
|
+
error: `Type failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
async function handleCheck(command) {
|
|
1738
|
+
const ref = command.ref;
|
|
1739
|
+
if (!ref) {
|
|
1740
|
+
return {
|
|
1741
|
+
id: command.id,
|
|
1742
|
+
success: false,
|
|
1743
|
+
error: "Missing ref parameter"
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1747
|
+
if (!activeTab || !activeTab.id) {
|
|
1748
|
+
return {
|
|
1749
|
+
id: command.id,
|
|
1750
|
+
success: false,
|
|
1751
|
+
error: "No active tab found"
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
console.log("[CommandHandler] Checking element:", ref);
|
|
1755
|
+
try {
|
|
1756
|
+
const elementInfo = await checkElement(activeTab.id, ref);
|
|
1757
|
+
return {
|
|
1758
|
+
id: command.id,
|
|
1759
|
+
success: true,
|
|
1760
|
+
data: {
|
|
1761
|
+
role: elementInfo.role,
|
|
1762
|
+
name: elementInfo.name,
|
|
1763
|
+
wasAlreadyChecked: elementInfo.wasAlreadyChecked
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
console.error("[CommandHandler] Check failed:", error);
|
|
1768
|
+
return {
|
|
1769
|
+
id: command.id,
|
|
1770
|
+
success: false,
|
|
1771
|
+
error: `Check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
async function handleUncheck(command) {
|
|
1776
|
+
const ref = command.ref;
|
|
1777
|
+
if (!ref) {
|
|
1778
|
+
return {
|
|
1779
|
+
id: command.id,
|
|
1780
|
+
success: false,
|
|
1781
|
+
error: "Missing ref parameter"
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1785
|
+
if (!activeTab || !activeTab.id) {
|
|
1786
|
+
return {
|
|
1787
|
+
id: command.id,
|
|
1788
|
+
success: false,
|
|
1789
|
+
error: "No active tab found"
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
console.log("[CommandHandler] Unchecking element:", ref);
|
|
1793
|
+
try {
|
|
1794
|
+
const elementInfo = await uncheckElement(activeTab.id, ref);
|
|
1795
|
+
return {
|
|
1796
|
+
id: command.id,
|
|
1797
|
+
success: true,
|
|
1798
|
+
data: {
|
|
1799
|
+
role: elementInfo.role,
|
|
1800
|
+
name: elementInfo.name,
|
|
1801
|
+
wasAlreadyUnchecked: elementInfo.wasAlreadyUnchecked
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
console.error("[CommandHandler] Uncheck failed:", error);
|
|
1806
|
+
return {
|
|
1807
|
+
id: command.id,
|
|
1808
|
+
success: false,
|
|
1809
|
+
error: `Uncheck failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
async function handleSelect(command) {
|
|
1814
|
+
const ref = command.ref;
|
|
1815
|
+
const value = command.value;
|
|
1816
|
+
if (!ref) {
|
|
1817
|
+
return {
|
|
1818
|
+
id: command.id,
|
|
1819
|
+
success: false,
|
|
1820
|
+
error: "Missing ref parameter"
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
if (value === void 0 || value === null) {
|
|
1824
|
+
return {
|
|
1825
|
+
id: command.id,
|
|
1826
|
+
success: false,
|
|
1827
|
+
error: "Missing value parameter"
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1831
|
+
if (!activeTab || !activeTab.id) {
|
|
1832
|
+
return {
|
|
1833
|
+
id: command.id,
|
|
1834
|
+
success: false,
|
|
1835
|
+
error: "No active tab found"
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
console.log("[CommandHandler] Selecting option:", ref, "value:", value);
|
|
1839
|
+
try {
|
|
1840
|
+
const result = await selectOption(activeTab.id, ref, value);
|
|
1841
|
+
return {
|
|
1842
|
+
id: command.id,
|
|
1843
|
+
success: true,
|
|
1844
|
+
data: {
|
|
1845
|
+
role: result.role,
|
|
1846
|
+
name: result.name,
|
|
1847
|
+
selectedValue: result.selectedValue,
|
|
1848
|
+
selectedLabel: result.selectedLabel
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
console.error("[CommandHandler] Select failed:", error);
|
|
1853
|
+
return {
|
|
1854
|
+
id: command.id,
|
|
1855
|
+
success: false,
|
|
1856
|
+
error: `Select failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
async function handleClose(command) {
|
|
1861
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1862
|
+
if (!activeTab || !activeTab.id) {
|
|
1863
|
+
return {
|
|
1864
|
+
id: command.id,
|
|
1865
|
+
success: false,
|
|
1866
|
+
error: "No active tab found"
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
const tabId = activeTab.id;
|
|
1870
|
+
const title = activeTab.title || "";
|
|
1871
|
+
const url = activeTab.url || "";
|
|
1872
|
+
console.log("[CommandHandler] Closing tab:", tabId, url);
|
|
1873
|
+
try {
|
|
1874
|
+
await chrome.tabs.remove(tabId);
|
|
1875
|
+
return {
|
|
1876
|
+
id: command.id,
|
|
1877
|
+
success: true,
|
|
1878
|
+
data: {
|
|
1879
|
+
tabId,
|
|
1880
|
+
title,
|
|
1881
|
+
url
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
console.error("[CommandHandler] Close failed:", error);
|
|
1886
|
+
return {
|
|
1887
|
+
id: command.id,
|
|
1888
|
+
success: false,
|
|
1889
|
+
error: `Close failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
async function handleGet(command) {
|
|
1894
|
+
const attribute = command.attribute;
|
|
1895
|
+
if (!attribute) {
|
|
1896
|
+
return {
|
|
1897
|
+
id: command.id,
|
|
1898
|
+
success: false,
|
|
1899
|
+
error: "Missing attribute parameter"
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1903
|
+
if (!activeTab || !activeTab.id) {
|
|
1904
|
+
return {
|
|
1905
|
+
id: command.id,
|
|
1906
|
+
success: false,
|
|
1907
|
+
error: "No active tab found"
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
console.log("[CommandHandler] Getting:", attribute);
|
|
1911
|
+
try {
|
|
1912
|
+
let value;
|
|
1913
|
+
switch (attribute) {
|
|
1914
|
+
case "url":
|
|
1915
|
+
value = activeTab.url || "";
|
|
1916
|
+
break;
|
|
1917
|
+
case "title":
|
|
1918
|
+
value = activeTab.title || "";
|
|
1919
|
+
break;
|
|
1920
|
+
case "text": {
|
|
1921
|
+
const ref = command.ref;
|
|
1922
|
+
if (!ref) {
|
|
1923
|
+
return {
|
|
1924
|
+
id: command.id,
|
|
1925
|
+
success: false,
|
|
1926
|
+
error: "Missing ref parameter for get text"
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
value = await getElementText(activeTab.id, ref);
|
|
1930
|
+
break;
|
|
1931
|
+
}
|
|
1932
|
+
default:
|
|
1933
|
+
return {
|
|
1934
|
+
id: command.id,
|
|
1935
|
+
success: false,
|
|
1936
|
+
error: `Unknown attribute: ${attribute}`
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
return {
|
|
1940
|
+
id: command.id,
|
|
1941
|
+
success: true,
|
|
1942
|
+
data: {
|
|
1943
|
+
value
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
console.error("[CommandHandler] Get failed:", error);
|
|
1948
|
+
return {
|
|
1949
|
+
id: command.id,
|
|
1950
|
+
success: false,
|
|
1951
|
+
error: `Get failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
async function handleScreenshot(command) {
|
|
1956
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1957
|
+
if (!activeTab || !activeTab.id || !activeTab.windowId) {
|
|
1958
|
+
return {
|
|
1959
|
+
id: command.id,
|
|
1960
|
+
success: false,
|
|
1961
|
+
error: "No active tab found"
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
console.log("[CommandHandler] Taking screenshot of tab:", activeTab.id, activeTab.url);
|
|
1965
|
+
try {
|
|
1966
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(activeTab.windowId, { format: "png" });
|
|
1967
|
+
return {
|
|
1968
|
+
id: command.id,
|
|
1969
|
+
success: true,
|
|
1970
|
+
data: {
|
|
1971
|
+
dataUrl,
|
|
1972
|
+
title: activeTab.title || "",
|
|
1973
|
+
url: activeTab.url || ""
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
console.error("[CommandHandler] Screenshot failed:", error);
|
|
1978
|
+
return {
|
|
1979
|
+
id: command.id,
|
|
1980
|
+
success: false,
|
|
1981
|
+
error: `Screenshot failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
async function handleWait(command) {
|
|
1986
|
+
const waitType = command.waitType;
|
|
1987
|
+
if (waitType === "time") {
|
|
1988
|
+
const ms = command.ms;
|
|
1989
|
+
if (!ms || ms < 0) {
|
|
1990
|
+
return {
|
|
1991
|
+
id: command.id,
|
|
1992
|
+
success: false,
|
|
1993
|
+
error: "Invalid ms parameter"
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
console.log("[CommandHandler] Waiting for", ms, "ms");
|
|
1997
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1998
|
+
return {
|
|
1999
|
+
id: command.id,
|
|
2000
|
+
success: true,
|
|
2001
|
+
data: { waited: ms }
|
|
2002
|
+
};
|
|
2003
|
+
} else if (waitType === "element") {
|
|
2004
|
+
const ref = command.ref;
|
|
2005
|
+
if (!ref) {
|
|
2006
|
+
return {
|
|
2007
|
+
id: command.id,
|
|
2008
|
+
success: false,
|
|
2009
|
+
error: "Missing ref parameter"
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2013
|
+
if (!activeTab || !activeTab.id) {
|
|
2014
|
+
return {
|
|
2015
|
+
id: command.id,
|
|
2016
|
+
success: false,
|
|
2017
|
+
error: "No active tab found"
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
console.log("[CommandHandler] Waiting for element:", ref);
|
|
2021
|
+
try {
|
|
2022
|
+
await waitForElement(activeTab.id, ref);
|
|
2023
|
+
return {
|
|
2024
|
+
id: command.id,
|
|
2025
|
+
success: true,
|
|
2026
|
+
data: { ref }
|
|
2027
|
+
};
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
console.error("[CommandHandler] Wait failed:", error);
|
|
2030
|
+
return {
|
|
2031
|
+
id: command.id,
|
|
2032
|
+
success: false,
|
|
2033
|
+
error: `Wait failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
} else {
|
|
2037
|
+
return {
|
|
2038
|
+
id: command.id,
|
|
2039
|
+
success: false,
|
|
2040
|
+
error: `Unknown wait type: ${waitType}`
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
async function handlePress(command) {
|
|
2045
|
+
const key = command.key;
|
|
2046
|
+
const modifiers = command.modifiers || [];
|
|
2047
|
+
if (!key) {
|
|
2048
|
+
return {
|
|
2049
|
+
id: command.id,
|
|
2050
|
+
success: false,
|
|
2051
|
+
error: "Missing key parameter"
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2055
|
+
if (!activeTab || !activeTab.id) {
|
|
2056
|
+
return {
|
|
2057
|
+
id: command.id,
|
|
2058
|
+
success: false,
|
|
2059
|
+
error: "No active tab found"
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
const url = activeTab.url || "";
|
|
2063
|
+
if (url.startsWith("chrome://") || url.startsWith("about:") || url.startsWith("chrome-extension://")) {
|
|
2064
|
+
return {
|
|
2065
|
+
id: command.id,
|
|
2066
|
+
success: false,
|
|
2067
|
+
error: `Cannot send keys to restricted page: ${url}`
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
console.log("[CommandHandler] Pressing key:", key, "modifiers:", modifiers);
|
|
2071
|
+
try {
|
|
2072
|
+
await pressKey(activeTab.id, key, modifiers);
|
|
2073
|
+
const displayKey = modifiers.length > 0 ? `${modifiers.join("+")}+${key}` : key;
|
|
2074
|
+
return {
|
|
2075
|
+
id: command.id,
|
|
2076
|
+
success: true,
|
|
2077
|
+
data: {
|
|
2078
|
+
key: displayKey
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
console.error("[CommandHandler] Press failed:", error);
|
|
2083
|
+
return {
|
|
2084
|
+
id: command.id,
|
|
2085
|
+
success: false,
|
|
2086
|
+
error: `Press failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
async function handleScroll(command) {
|
|
2091
|
+
const direction = command.direction;
|
|
2092
|
+
const pixels = command.pixels || 300;
|
|
2093
|
+
if (!direction) {
|
|
2094
|
+
return {
|
|
2095
|
+
id: command.id,
|
|
2096
|
+
success: false,
|
|
2097
|
+
error: "Missing direction parameter"
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
if (!["up", "down", "left", "right"].includes(direction)) {
|
|
2101
|
+
return {
|
|
2102
|
+
id: command.id,
|
|
2103
|
+
success: false,
|
|
2104
|
+
error: `Invalid direction: ${direction}`
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2108
|
+
if (!activeTab || !activeTab.id) {
|
|
2109
|
+
return {
|
|
2110
|
+
id: command.id,
|
|
2111
|
+
success: false,
|
|
2112
|
+
error: "No active tab found"
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
console.log("[CommandHandler] Scrolling:", direction, pixels, "px");
|
|
2116
|
+
try {
|
|
2117
|
+
await scrollPage(activeTab.id, direction, pixels);
|
|
2118
|
+
return {
|
|
2119
|
+
id: command.id,
|
|
2120
|
+
success: true,
|
|
2121
|
+
data: {
|
|
2122
|
+
direction,
|
|
2123
|
+
pixels
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
console.error("[CommandHandler] Scroll failed:", error);
|
|
2128
|
+
return {
|
|
2129
|
+
id: command.id,
|
|
2130
|
+
success: false,
|
|
2131
|
+
error: `Scroll failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async function handleBack(command) {
|
|
2136
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2137
|
+
if (!activeTab || !activeTab.id) {
|
|
2138
|
+
return {
|
|
2139
|
+
id: command.id,
|
|
2140
|
+
success: false,
|
|
2141
|
+
error: "No active tab found"
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
const tabId = activeTab.id;
|
|
2145
|
+
console.log("[CommandHandler] Going back in tab:", tabId);
|
|
2146
|
+
try {
|
|
2147
|
+
const canGoBack = await evaluate(tabId, "window.history.length > 1");
|
|
2148
|
+
if (!canGoBack) {
|
|
2149
|
+
return {
|
|
2150
|
+
id: command.id,
|
|
2151
|
+
success: false,
|
|
2152
|
+
error: "No previous page in history"
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
await evaluate(tabId, "window.history.back()");
|
|
2156
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
2157
|
+
const updatedTab = await chrome.tabs.get(tabId);
|
|
2158
|
+
return {
|
|
2159
|
+
id: command.id,
|
|
2160
|
+
success: true,
|
|
2161
|
+
data: {
|
|
2162
|
+
url: updatedTab.url || "",
|
|
2163
|
+
title: updatedTab.title || ""
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
console.error("[CommandHandler] Back failed:", error);
|
|
2168
|
+
return {
|
|
2169
|
+
id: command.id,
|
|
2170
|
+
success: false,
|
|
2171
|
+
error: `Back failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
async function handleForward(command) {
|
|
2176
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2177
|
+
if (!activeTab || !activeTab.id) {
|
|
2178
|
+
return {
|
|
2179
|
+
id: command.id,
|
|
2180
|
+
success: false,
|
|
2181
|
+
error: "No active tab found"
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
const tabId = activeTab.id;
|
|
2185
|
+
console.log("[CommandHandler] Going forward in tab:", tabId);
|
|
2186
|
+
try {
|
|
2187
|
+
await evaluate(tabId, "window.history.forward()");
|
|
2188
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
2189
|
+
const updatedTab = await chrome.tabs.get(tabId);
|
|
2190
|
+
return {
|
|
2191
|
+
id: command.id,
|
|
2192
|
+
success: true,
|
|
2193
|
+
data: {
|
|
2194
|
+
url: updatedTab.url || "",
|
|
2195
|
+
title: updatedTab.title || ""
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
console.error("[CommandHandler] Forward failed:", error);
|
|
2200
|
+
return {
|
|
2201
|
+
id: command.id,
|
|
2202
|
+
success: false,
|
|
2203
|
+
error: `Forward failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
async function handleRefresh(command) {
|
|
2208
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2209
|
+
if (!activeTab || !activeTab.id) {
|
|
2210
|
+
return {
|
|
2211
|
+
id: command.id,
|
|
2212
|
+
success: false,
|
|
2213
|
+
error: "No active tab found"
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
console.log("[CommandHandler] Refreshing tab:", activeTab.id);
|
|
2217
|
+
try {
|
|
2218
|
+
await chrome.tabs.reload(activeTab.id);
|
|
2219
|
+
await waitForTabLoad(activeTab.id);
|
|
2220
|
+
const updatedTab = await chrome.tabs.get(activeTab.id);
|
|
2221
|
+
return {
|
|
2222
|
+
id: command.id,
|
|
2223
|
+
success: true,
|
|
2224
|
+
data: {
|
|
2225
|
+
url: updatedTab.url || "",
|
|
2226
|
+
title: updatedTab.title || ""
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
console.error("[CommandHandler] Refresh failed:", error);
|
|
2231
|
+
return {
|
|
2232
|
+
id: command.id,
|
|
2233
|
+
success: false,
|
|
2234
|
+
error: `Refresh failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
async function handleEval(command) {
|
|
2239
|
+
const script = command.script;
|
|
2240
|
+
if (!script) {
|
|
2241
|
+
return {
|
|
2242
|
+
id: command.id,
|
|
2243
|
+
success: false,
|
|
2244
|
+
error: "Missing script parameter"
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2248
|
+
if (!activeTab || !activeTab.id) {
|
|
2249
|
+
return {
|
|
2250
|
+
id: command.id,
|
|
2251
|
+
success: false,
|
|
2252
|
+
error: "No active tab found"
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
const url = activeTab.url || "";
|
|
2256
|
+
if (url.startsWith("chrome://") || url.startsWith("about:") || url.startsWith("chrome-extension://")) {
|
|
2257
|
+
return {
|
|
2258
|
+
id: command.id,
|
|
2259
|
+
success: false,
|
|
2260
|
+
error: `Cannot execute script on restricted page: ${url}`
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
console.log("[CommandHandler] Evaluating script:", script.substring(0, 100));
|
|
2264
|
+
const tabId = activeTab.id;
|
|
2265
|
+
try {
|
|
2266
|
+
const result = await evaluate(tabId, script);
|
|
2267
|
+
console.log("[CommandHandler] Eval result:", JSON.stringify(result));
|
|
2268
|
+
return {
|
|
2269
|
+
id: command.id,
|
|
2270
|
+
success: true,
|
|
2271
|
+
data: {
|
|
2272
|
+
result
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
console.error("[CommandHandler] Eval failed:", error);
|
|
2277
|
+
return {
|
|
2278
|
+
id: command.id,
|
|
2279
|
+
success: false,
|
|
2280
|
+
error: `Eval failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
async function handleTabList(command) {
|
|
2285
|
+
console.log("[CommandHandler] Listing all tabs");
|
|
2286
|
+
try {
|
|
2287
|
+
const tabs = await chrome.tabs.query({ currentWindow: true });
|
|
2288
|
+
const tabInfos = tabs.map((tab) => ({
|
|
2289
|
+
index: tab.index,
|
|
2290
|
+
url: tab.url || "",
|
|
2291
|
+
title: tab.title || "",
|
|
2292
|
+
active: tab.active || false,
|
|
2293
|
+
tabId: tab.id || 0
|
|
2294
|
+
}));
|
|
2295
|
+
const activeTab = tabInfos.find((t) => t.active);
|
|
2296
|
+
const activeIndex = activeTab?.index ?? 0;
|
|
2297
|
+
return {
|
|
2298
|
+
id: command.id,
|
|
2299
|
+
success: true,
|
|
2300
|
+
data: {
|
|
2301
|
+
tabs: tabInfos,
|
|
2302
|
+
activeIndex
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
console.error("[CommandHandler] Tab list failed:", error);
|
|
2307
|
+
return {
|
|
2308
|
+
id: command.id,
|
|
2309
|
+
success: false,
|
|
2310
|
+
error: `Tab list failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
async function handleTabNew(command) {
|
|
2315
|
+
const url = command.url;
|
|
2316
|
+
console.log("[CommandHandler] Creating new tab:", url || "about:blank");
|
|
2317
|
+
try {
|
|
2318
|
+
const createOptions = { active: true };
|
|
2319
|
+
if (url) {
|
|
2320
|
+
createOptions.url = url;
|
|
2321
|
+
}
|
|
2322
|
+
const tab = await chrome.tabs.create(createOptions);
|
|
2323
|
+
if (url && tab.id) {
|
|
2324
|
+
await waitForTabLoad(tab.id);
|
|
2325
|
+
}
|
|
2326
|
+
const updatedTab = tab.id ? await chrome.tabs.get(tab.id) : tab;
|
|
2327
|
+
return {
|
|
2328
|
+
id: command.id,
|
|
2329
|
+
success: true,
|
|
2330
|
+
data: {
|
|
2331
|
+
tabId: updatedTab.id,
|
|
2332
|
+
title: updatedTab.title || "",
|
|
2333
|
+
url: updatedTab.url || ""
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
} catch (error) {
|
|
2337
|
+
console.error("[CommandHandler] Tab new failed:", error);
|
|
2338
|
+
return {
|
|
2339
|
+
id: command.id,
|
|
2340
|
+
success: false,
|
|
2341
|
+
error: `Tab new failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
async function handleTabSelect(command) {
|
|
2346
|
+
const index = command.index;
|
|
2347
|
+
if (index === void 0 || index < 0) {
|
|
2348
|
+
return {
|
|
2349
|
+
id: command.id,
|
|
2350
|
+
success: false,
|
|
2351
|
+
error: "Missing or invalid index parameter"
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
console.log("[CommandHandler] Selecting tab at index:", index);
|
|
2355
|
+
try {
|
|
2356
|
+
const tabs = await chrome.tabs.query({ currentWindow: true });
|
|
2357
|
+
const targetTab = tabs.find((t) => t.index === index);
|
|
2358
|
+
if (!targetTab || !targetTab.id) {
|
|
2359
|
+
return {
|
|
2360
|
+
id: command.id,
|
|
2361
|
+
success: false,
|
|
2362
|
+
error: `No tab found at index ${index} (total tabs: ${tabs.length})`
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
await chrome.tabs.update(targetTab.id, { active: true });
|
|
2366
|
+
return {
|
|
2367
|
+
id: command.id,
|
|
2368
|
+
success: true,
|
|
2369
|
+
data: {
|
|
2370
|
+
tabId: targetTab.id,
|
|
2371
|
+
title: targetTab.title || "",
|
|
2372
|
+
url: targetTab.url || ""
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
console.error("[CommandHandler] Tab select failed:", error);
|
|
2377
|
+
return {
|
|
2378
|
+
id: command.id,
|
|
2379
|
+
success: false,
|
|
2380
|
+
error: `Tab select failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
async function handleTabClose(command) {
|
|
2385
|
+
const index = command.index;
|
|
2386
|
+
console.log("[CommandHandler] Closing tab at index:", index ?? "current");
|
|
2387
|
+
try {
|
|
2388
|
+
let targetTab;
|
|
2389
|
+
if (index !== void 0) {
|
|
2390
|
+
const tabs = await chrome.tabs.query({ currentWindow: true });
|
|
2391
|
+
const found = tabs.find((t) => t.index === index);
|
|
2392
|
+
if (!found || !found.id) {
|
|
2393
|
+
return {
|
|
2394
|
+
id: command.id,
|
|
2395
|
+
success: false,
|
|
2396
|
+
error: `No tab found at index ${index} (total tabs: ${tabs.length})`
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
targetTab = found;
|
|
2400
|
+
} else {
|
|
2401
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2402
|
+
if (!activeTab || !activeTab.id) {
|
|
2403
|
+
return {
|
|
2404
|
+
id: command.id,
|
|
2405
|
+
success: false,
|
|
2406
|
+
error: "No active tab found"
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
targetTab = activeTab;
|
|
2410
|
+
}
|
|
2411
|
+
const tabId = targetTab.id;
|
|
2412
|
+
const title = targetTab.title || "";
|
|
2413
|
+
const url = targetTab.url || "";
|
|
2414
|
+
await chrome.tabs.remove(tabId);
|
|
2415
|
+
return {
|
|
2416
|
+
id: command.id,
|
|
2417
|
+
success: true,
|
|
2418
|
+
data: {
|
|
2419
|
+
tabId,
|
|
2420
|
+
title,
|
|
2421
|
+
url
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
console.error("[CommandHandler] Tab close failed:", error);
|
|
2426
|
+
return {
|
|
2427
|
+
id: command.id,
|
|
2428
|
+
success: false,
|
|
2429
|
+
error: `Tab close failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
async function handleFrame(command) {
|
|
2434
|
+
const selector = command.selector;
|
|
2435
|
+
if (!selector) {
|
|
2436
|
+
return {
|
|
2437
|
+
id: command.id,
|
|
2438
|
+
success: false,
|
|
2439
|
+
error: "Missing selector parameter"
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2443
|
+
if (!activeTab || !activeTab.id) {
|
|
2444
|
+
return {
|
|
2445
|
+
id: command.id,
|
|
2446
|
+
success: false,
|
|
2447
|
+
error: "No active tab found"
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
const tabId = activeTab.id;
|
|
2451
|
+
console.log("[CommandHandler] Switching to frame:", selector);
|
|
2452
|
+
try {
|
|
2453
|
+
const iframeInfoResults = await chrome.scripting.executeScript({
|
|
2454
|
+
target: { tabId, frameIds: activeFrameId !== null ? [activeFrameId] : [0] },
|
|
2455
|
+
func: (sel) => {
|
|
2456
|
+
const iframe = document.querySelector(sel);
|
|
2457
|
+
if (!iframe) {
|
|
2458
|
+
return { found: false, error: `找不到 iframe: ${sel}` };
|
|
2459
|
+
}
|
|
2460
|
+
if (iframe.tagName.toLowerCase() !== "iframe" && iframe.tagName.toLowerCase() !== "frame") {
|
|
2461
|
+
return { found: false, error: `元素不是 iframe: ${iframe.tagName}` };
|
|
2462
|
+
}
|
|
2463
|
+
return {
|
|
2464
|
+
found: true,
|
|
2465
|
+
name: iframe.name || "",
|
|
2466
|
+
src: iframe.src || "",
|
|
2467
|
+
// 获取 iframe 在页面中的位置用于匹配
|
|
2468
|
+
rect: iframe.getBoundingClientRect()
|
|
2469
|
+
};
|
|
2470
|
+
},
|
|
2471
|
+
args: [selector]
|
|
2472
|
+
});
|
|
2473
|
+
const iframeInfo = iframeInfoResults[0]?.result;
|
|
2474
|
+
if (!iframeInfo || !iframeInfo.found) {
|
|
2475
|
+
return {
|
|
2476
|
+
id: command.id,
|
|
2477
|
+
success: false,
|
|
2478
|
+
error: iframeInfo?.error || `找不到 iframe: ${selector}`
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
const frames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
2482
|
+
if (!frames || frames.length === 0) {
|
|
2483
|
+
return {
|
|
2484
|
+
id: command.id,
|
|
2485
|
+
success: false,
|
|
2486
|
+
error: "无法获取页面 frames"
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
let targetFrameId = null;
|
|
2490
|
+
if (iframeInfo.src) {
|
|
2491
|
+
const matchedFrame = frames.find(
|
|
2492
|
+
(f) => f.url === iframeInfo.src || f.url.includes(iframeInfo.src) || iframeInfo.src.includes(f.url)
|
|
2493
|
+
);
|
|
2494
|
+
if (matchedFrame) {
|
|
2495
|
+
targetFrameId = matchedFrame.frameId;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (targetFrameId === null) {
|
|
2499
|
+
const childFrames = frames.filter((f) => f.frameId !== 0);
|
|
2500
|
+
if (childFrames.length === 1) {
|
|
2501
|
+
targetFrameId = childFrames[0].frameId;
|
|
2502
|
+
} else if (childFrames.length > 1) {
|
|
2503
|
+
if (iframeInfo.name) {
|
|
2504
|
+
console.log("[CommandHandler] Multiple frames found, using URL matching");
|
|
2505
|
+
}
|
|
2506
|
+
if (targetFrameId === null) {
|
|
2507
|
+
return {
|
|
2508
|
+
id: command.id,
|
|
2509
|
+
success: false,
|
|
2510
|
+
error: `找到多个子 frame,无法确定目标。请使用更精确的 selector 或确保 iframe 有 src 属性。`
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
} else {
|
|
2514
|
+
return {
|
|
2515
|
+
id: command.id,
|
|
2516
|
+
success: false,
|
|
2517
|
+
error: "页面中没有子 frame"
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
await chrome.scripting.executeScript({
|
|
2523
|
+
target: { tabId, frameIds: [targetFrameId] },
|
|
2524
|
+
func: () => true
|
|
2525
|
+
});
|
|
2526
|
+
} catch (e) {
|
|
2527
|
+
return {
|
|
2528
|
+
id: command.id,
|
|
2529
|
+
success: false,
|
|
2530
|
+
error: `无法访问 frame (frameId: ${targetFrameId}),可能是跨域 iframe`
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
activeFrameId = targetFrameId;
|
|
2534
|
+
setActiveFrameId(String(targetFrameId));
|
|
2535
|
+
const matchedFrameInfo = frames.find((f) => f.frameId === targetFrameId);
|
|
2536
|
+
return {
|
|
2537
|
+
id: command.id,
|
|
2538
|
+
success: true,
|
|
2539
|
+
data: {
|
|
2540
|
+
frameInfo: {
|
|
2541
|
+
selector,
|
|
2542
|
+
name: iframeInfo.name,
|
|
2543
|
+
url: matchedFrameInfo?.url || iframeInfo.src,
|
|
2544
|
+
frameId: targetFrameId
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
console.error("[CommandHandler] Frame switch failed:", error);
|
|
2550
|
+
return {
|
|
2551
|
+
id: command.id,
|
|
2552
|
+
success: false,
|
|
2553
|
+
error: `Frame switch failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
async function handleFrameMain(command) {
|
|
2558
|
+
console.log("[CommandHandler] Switching to main frame");
|
|
2559
|
+
activeFrameId = null;
|
|
2560
|
+
setActiveFrameId(null);
|
|
2561
|
+
return {
|
|
2562
|
+
id: command.id,
|
|
2563
|
+
success: true,
|
|
2564
|
+
data: {
|
|
2565
|
+
frameInfo: {
|
|
2566
|
+
frameId: 0
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
async function handleDialog(command) {
|
|
2572
|
+
const dialogResponse = command.dialogResponse;
|
|
2573
|
+
const promptText = command.promptText;
|
|
2574
|
+
if (!dialogResponse || !["accept", "dismiss"].includes(dialogResponse)) {
|
|
2575
|
+
return {
|
|
2576
|
+
id: command.id,
|
|
2577
|
+
success: false,
|
|
2578
|
+
error: "Missing or invalid dialogResponse parameter (accept/dismiss)"
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2582
|
+
if (!activeTab || !activeTab.id) {
|
|
2583
|
+
return {
|
|
2584
|
+
id: command.id,
|
|
2585
|
+
success: false,
|
|
2586
|
+
error: "No active tab found"
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
const tabId = activeTab.id;
|
|
2590
|
+
console.log("[CommandHandler] Handling dialog:", dialogResponse, "promptText:", promptText);
|
|
2591
|
+
try {
|
|
2592
|
+
const pendingDialog = getPendingDialog(tabId);
|
|
2593
|
+
if (!pendingDialog) {
|
|
2594
|
+
return {
|
|
2595
|
+
id: command.id,
|
|
2596
|
+
success: false,
|
|
2597
|
+
error: "没有待处理的对话框"
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
await handleJavaScriptDialog(
|
|
2601
|
+
tabId,
|
|
2602
|
+
dialogResponse === "accept",
|
|
2603
|
+
dialogResponse === "accept" ? promptText : void 0
|
|
2604
|
+
);
|
|
2605
|
+
const dialogInfo = {
|
|
2606
|
+
type: pendingDialog.type,
|
|
2607
|
+
message: pendingDialog.message,
|
|
2608
|
+
handled: true
|
|
2609
|
+
};
|
|
2610
|
+
return {
|
|
2611
|
+
id: command.id,
|
|
2612
|
+
success: true,
|
|
2613
|
+
data: {
|
|
2614
|
+
dialogInfo
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
console.error("[CommandHandler] Dialog handling failed:", error);
|
|
2619
|
+
return {
|
|
2620
|
+
id: command.id,
|
|
2621
|
+
success: false,
|
|
2622
|
+
error: `Dialog failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
function waitForTabLoad(tabId, timeout = 3e4) {
|
|
2627
|
+
return new Promise((resolve, reject) => {
|
|
2628
|
+
const timeoutId = setTimeout(() => {
|
|
2629
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
2630
|
+
reject(new Error("Tab load timeout"));
|
|
2631
|
+
}, timeout);
|
|
2632
|
+
const listener = (updatedTabId, changeInfo) => {
|
|
2633
|
+
if (updatedTabId === tabId && changeInfo.status === "complete") {
|
|
2634
|
+
clearTimeout(timeoutId);
|
|
2635
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
2636
|
+
resolve();
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
async function handleNetwork(command) {
|
|
2643
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2644
|
+
if (!activeTab || !activeTab.id) {
|
|
2645
|
+
return {
|
|
2646
|
+
id: command.id,
|
|
2647
|
+
success: false,
|
|
2648
|
+
error: "No active tab found"
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
const tabId = activeTab.id;
|
|
2652
|
+
const subCommand = command.networkCommand;
|
|
2653
|
+
const urlPattern = command.url;
|
|
2654
|
+
console.log("[CommandHandler] Network command:", subCommand, urlPattern);
|
|
2655
|
+
try {
|
|
2656
|
+
switch (subCommand) {
|
|
2657
|
+
case "requests": {
|
|
2658
|
+
await enableNetwork(tabId);
|
|
2659
|
+
const filter = command.filter;
|
|
2660
|
+
const requests = getNetworkRequests(tabId, filter);
|
|
2661
|
+
const networkRequests = requests.map((r) => ({
|
|
2662
|
+
requestId: r.requestId,
|
|
2663
|
+
url: r.url,
|
|
2664
|
+
method: r.method,
|
|
2665
|
+
type: r.type,
|
|
2666
|
+
timestamp: r.timestamp,
|
|
2667
|
+
status: r.response?.status,
|
|
2668
|
+
statusText: r.response?.statusText,
|
|
2669
|
+
failed: r.failed,
|
|
2670
|
+
failureReason: r.failureReason
|
|
2671
|
+
}));
|
|
2672
|
+
return {
|
|
2673
|
+
id: command.id,
|
|
2674
|
+
success: true,
|
|
2675
|
+
data: {
|
|
2676
|
+
networkRequests
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
case "route": {
|
|
2681
|
+
if (!urlPattern) {
|
|
2682
|
+
return {
|
|
2683
|
+
id: command.id,
|
|
2684
|
+
success: false,
|
|
2685
|
+
error: "URL pattern required for route command"
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
const options = command.routeOptions || {};
|
|
2689
|
+
await addNetworkRoute(tabId, urlPattern, options);
|
|
2690
|
+
const routeCount = getNetworkRoutes(tabId).length;
|
|
2691
|
+
return {
|
|
2692
|
+
id: command.id,
|
|
2693
|
+
success: true,
|
|
2694
|
+
data: {
|
|
2695
|
+
routeCount
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
case "unroute": {
|
|
2700
|
+
removeNetworkRoute(tabId, urlPattern);
|
|
2701
|
+
const routeCount = getNetworkRoutes(tabId).length;
|
|
2702
|
+
return {
|
|
2703
|
+
id: command.id,
|
|
2704
|
+
success: true,
|
|
2705
|
+
data: {
|
|
2706
|
+
routeCount
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
case "clear": {
|
|
2711
|
+
clearNetworkRequests(tabId);
|
|
2712
|
+
return {
|
|
2713
|
+
id: command.id,
|
|
2714
|
+
success: true,
|
|
2715
|
+
data: {}
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
default:
|
|
2719
|
+
return {
|
|
2720
|
+
id: command.id,
|
|
2721
|
+
success: false,
|
|
2722
|
+
error: `Unknown network subcommand: ${subCommand}`
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
console.error("[CommandHandler] Network command failed:", error);
|
|
2727
|
+
return {
|
|
2728
|
+
id: command.id,
|
|
2729
|
+
success: false,
|
|
2730
|
+
error: `Network command failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
async function handleConsole(command) {
|
|
2735
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2736
|
+
if (!activeTab || !activeTab.id) {
|
|
2737
|
+
return {
|
|
2738
|
+
id: command.id,
|
|
2739
|
+
success: false,
|
|
2740
|
+
error: "No active tab found"
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
const tabId = activeTab.id;
|
|
2744
|
+
const subCommand = command.consoleCommand || "get";
|
|
2745
|
+
console.log("[CommandHandler] Console command:", subCommand);
|
|
2746
|
+
try {
|
|
2747
|
+
await enableConsole(tabId);
|
|
2748
|
+
switch (subCommand) {
|
|
2749
|
+
case "get": {
|
|
2750
|
+
const messages = getConsoleMessages(tabId);
|
|
2751
|
+
return {
|
|
2752
|
+
id: command.id,
|
|
2753
|
+
success: true,
|
|
2754
|
+
data: {
|
|
2755
|
+
consoleMessages: messages
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
case "clear": {
|
|
2760
|
+
clearConsoleMessages(tabId);
|
|
2761
|
+
return {
|
|
2762
|
+
id: command.id,
|
|
2763
|
+
success: true,
|
|
2764
|
+
data: {}
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
default:
|
|
2768
|
+
return {
|
|
2769
|
+
id: command.id,
|
|
2770
|
+
success: false,
|
|
2771
|
+
error: `Unknown console subcommand: ${subCommand}`
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
console.error("[CommandHandler] Console command failed:", error);
|
|
2776
|
+
return {
|
|
2777
|
+
id: command.id,
|
|
2778
|
+
success: false,
|
|
2779
|
+
error: `Console command failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
async function handleErrors(command) {
|
|
2784
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2785
|
+
if (!activeTab || !activeTab.id) {
|
|
2786
|
+
return {
|
|
2787
|
+
id: command.id,
|
|
2788
|
+
success: false,
|
|
2789
|
+
error: "No active tab found"
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
const tabId = activeTab.id;
|
|
2793
|
+
const subCommand = command.errorsCommand || "get";
|
|
2794
|
+
console.log("[CommandHandler] Errors command:", subCommand);
|
|
2795
|
+
try {
|
|
2796
|
+
await enableConsole(tabId);
|
|
2797
|
+
switch (subCommand) {
|
|
2798
|
+
case "get": {
|
|
2799
|
+
const errors = getJSErrors(tabId);
|
|
2800
|
+
return {
|
|
2801
|
+
id: command.id,
|
|
2802
|
+
success: true,
|
|
2803
|
+
data: {
|
|
2804
|
+
jsErrors: errors
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
case "clear": {
|
|
2809
|
+
clearJSErrors(tabId);
|
|
2810
|
+
return {
|
|
2811
|
+
id: command.id,
|
|
2812
|
+
success: true,
|
|
2813
|
+
data: {}
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
default:
|
|
2817
|
+
return {
|
|
2818
|
+
id: command.id,
|
|
2819
|
+
success: false,
|
|
2820
|
+
error: `Unknown errors subcommand: ${subCommand}`
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
} catch (error) {
|
|
2824
|
+
console.error("[CommandHandler] Errors command failed:", error);
|
|
2825
|
+
return {
|
|
2826
|
+
id: command.id,
|
|
2827
|
+
success: false,
|
|
2828
|
+
error: `Errors command failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
async function handleTrace(command) {
|
|
2833
|
+
const subCommand = command.traceCommand || "status";
|
|
2834
|
+
console.log("[CommandHandler] Trace command:", subCommand);
|
|
2835
|
+
try {
|
|
2836
|
+
switch (subCommand) {
|
|
2837
|
+
case "start": {
|
|
2838
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
2839
|
+
if (!activeTab || !activeTab.id) {
|
|
2840
|
+
return {
|
|
2841
|
+
id: command.id,
|
|
2842
|
+
success: false,
|
|
2843
|
+
error: "No active tab found"
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
const url = activeTab.url || "";
|
|
2847
|
+
if (url.startsWith("chrome://") || url.startsWith("about:") || url.startsWith("chrome-extension://")) {
|
|
2848
|
+
return {
|
|
2849
|
+
id: command.id,
|
|
2850
|
+
success: false,
|
|
2851
|
+
error: `Cannot record on restricted page: ${url}`
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
await startRecording(activeTab.id);
|
|
2855
|
+
const status = getStatus();
|
|
2856
|
+
return {
|
|
2857
|
+
id: command.id,
|
|
2858
|
+
success: true,
|
|
2859
|
+
data: {
|
|
2860
|
+
traceStatus: status
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
case "stop": {
|
|
2865
|
+
const events = await stopRecording();
|
|
2866
|
+
return {
|
|
2867
|
+
id: command.id,
|
|
2868
|
+
success: true,
|
|
2869
|
+
data: {
|
|
2870
|
+
traceEvents: events,
|
|
2871
|
+
traceStatus: {
|
|
2872
|
+
recording: false,
|
|
2873
|
+
eventCount: events.length
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
case "status": {
|
|
2879
|
+
const status = getStatus();
|
|
2880
|
+
return {
|
|
2881
|
+
id: command.id,
|
|
2882
|
+
success: true,
|
|
2883
|
+
data: {
|
|
2884
|
+
traceStatus: status
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
default:
|
|
2889
|
+
return {
|
|
2890
|
+
id: command.id,
|
|
2891
|
+
success: false,
|
|
2892
|
+
error: `Unknown trace subcommand: ${subCommand}`
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
console.error("[CommandHandler] Trace command failed:", error);
|
|
2897
|
+
return {
|
|
2898
|
+
id: command.id,
|
|
2899
|
+
success: false,
|
|
2900
|
+
error: `Trace command failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
const KEEPALIVE_ALARM = "bb-browser-keepalive";
|
|
2906
|
+
const sseClient = new SSEClient();
|
|
2907
|
+
sseClient.onCommand(handleCommand);
|
|
2908
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
2909
|
+
console.log("[bb-browser] Message from content script:", message, "sender:", sender.tab?.id);
|
|
2910
|
+
sendResponse({ received: true });
|
|
2911
|
+
return true;
|
|
2912
|
+
});
|
|
2913
|
+
async function setupKeepaliveAlarm() {
|
|
2914
|
+
await chrome.alarms.clear(KEEPALIVE_ALARM);
|
|
2915
|
+
await chrome.alarms.create(KEEPALIVE_ALARM, {
|
|
2916
|
+
periodInMinutes: 0.4
|
|
2917
|
+
// 24 秒
|
|
2918
|
+
});
|
|
2919
|
+
console.log("[bb-browser] Keepalive alarm set (every 24s)");
|
|
2920
|
+
}
|
|
2921
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
2922
|
+
if (alarm.name === KEEPALIVE_ALARM) {
|
|
2923
|
+
console.log("[bb-browser] Keepalive alarm triggered, checking connection...");
|
|
2924
|
+
if (!sseClient.isConnected()) {
|
|
2925
|
+
console.log("[bb-browser] SSE disconnected, reconnecting...");
|
|
2926
|
+
sseClient.connect();
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
chrome.runtime.onInstalled.addListener((details) => {
|
|
2931
|
+
console.log("[bb-browser] Extension installed/updated:", details.reason);
|
|
2932
|
+
sseClient.connect();
|
|
2933
|
+
setupKeepaliveAlarm();
|
|
2934
|
+
});
|
|
2935
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
2936
|
+
console.log("[bb-browser] Browser started, connecting to daemon...");
|
|
2937
|
+
sseClient.connect();
|
|
2938
|
+
setupKeepaliveAlarm();
|
|
2939
|
+
});
|
|
2940
|
+
console.log("[bb-browser] Background service worker started, connecting to daemon...");
|
|
2941
|
+
sseClient.connect();
|
|
2942
|
+
setupKeepaliveAlarm();
|
|
2943
|
+
//# sourceMappingURL=background.js.map
|