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.
@@ -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