browser-ctl 0.2.8__tar.gz → 0.2.9__tar.gz
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.
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/PKG-INFO +1 -1
- browser_ctl-0.2.9/browser_ctl/extension/actions.js +502 -0
- browser_ctl-0.2.9/browser_ctl/extension/background.js +258 -0
- browser_ctl-0.2.9/browser_ctl/extension/click.js +338 -0
- browser_ctl-0.2.9/browser_ctl/extension/content-script.js +1001 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/extension/manifest.json +3 -2
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl.egg-info/PKG-INFO +1 -1
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl.egg-info/SOURCES.txt +3 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/pyproject.toml +1 -1
- browser_ctl-0.2.8/browser_ctl/extension/background.js +0 -1396
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/LICENSE +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/README.md +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/SKILL.md +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/__init__.py +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/__main__.py +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/cli.py +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/client.py +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/extension/icon-128.png +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/extension/icon-16.png +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/extension/icon-32.png +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/extension/icon-48.png +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl/server.py +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl.egg-info/dependency_links.txt +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl.egg-info/entry_points.txt +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl.egg-info/requires.txt +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/browser_ctl.egg-info/top_level.txt +0 -0
- {browser_ctl-0.2.8 → browser_ctl-0.2.9}/setup.cfg +0 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* actions.js — Chrome API action handlers for Browser-Ctl.
|
|
3
|
+
*
|
|
4
|
+
* Handles navigation, tabs, screenshot, eval, download, upload, dialog,
|
|
5
|
+
* wait, and the runInPage/doBatch helpers for content-script operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { contentScriptHandler } from "./content-script.js";
|
|
9
|
+
|
|
10
|
+
// =========================================================================
|
|
11
|
+
// Shared helpers
|
|
12
|
+
// =========================================================================
|
|
13
|
+
|
|
14
|
+
/** Get the currently active tab. */
|
|
15
|
+
export async function activeTab() {
|
|
16
|
+
const [tab] = await chrome.tabs.query({
|
|
17
|
+
active: true,
|
|
18
|
+
currentWindow: true,
|
|
19
|
+
});
|
|
20
|
+
if (!tab) throw new Error("No active tab");
|
|
21
|
+
return tab;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wait for a tab to finish loading after navigation.
|
|
26
|
+
* Returns { loaded: boolean, timedOut: boolean } so callers can decide.
|
|
27
|
+
*/
|
|
28
|
+
export function waitForTabLoad(tabId, timeoutMs = 10000) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
32
|
+
resolve({ loaded: false, timedOut: true });
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
|
|
35
|
+
function listener(updatedTabId, info) {
|
|
36
|
+
if (updatedTabId === tabId && info.status === "complete") {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
39
|
+
resolve({ loaded: true, timedOut: false });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =========================================================================
|
|
47
|
+
// Content-script injection helpers
|
|
48
|
+
// =========================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run a single DOM operation in the active tab.
|
|
52
|
+
* Wraps the command as a 1-element batch and extracts the first result.
|
|
53
|
+
* Includes lightweight retry for transient service-worker issues.
|
|
54
|
+
*/
|
|
55
|
+
export async function runInPage(op, params) {
|
|
56
|
+
const tab = await activeTab();
|
|
57
|
+
|
|
58
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
59
|
+
try {
|
|
60
|
+
const results = await chrome.scripting.executeScript({
|
|
61
|
+
target: { tabId: tab.id },
|
|
62
|
+
func: contentScriptHandler,
|
|
63
|
+
args: [[{ op, params }]],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const arr = results[0]?.result;
|
|
67
|
+
if (!arr || !arr.length) {
|
|
68
|
+
if (attempt === 0) continue; // Retry once
|
|
69
|
+
throw new Error("Content script returned no result");
|
|
70
|
+
}
|
|
71
|
+
const r = arr[0];
|
|
72
|
+
if (!r.success)
|
|
73
|
+
throw new Error(r.error || "Content script operation failed");
|
|
74
|
+
return r.data;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (attempt === 0 && e.message?.includes("no result")) continue;
|
|
77
|
+
throw e;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Execute multiple DOM operations in a single executeScript call.
|
|
84
|
+
* Called by the "batch" action.
|
|
85
|
+
*/
|
|
86
|
+
export async function doBatch(params) {
|
|
87
|
+
const commands = params.commands || [];
|
|
88
|
+
if (!commands.length) return { results: [] };
|
|
89
|
+
|
|
90
|
+
const tab = await activeTab();
|
|
91
|
+
const ops = commands.map((c) => ({ op: c.action, params: c.params }));
|
|
92
|
+
|
|
93
|
+
const results = await chrome.scripting.executeScript({
|
|
94
|
+
target: { tabId: tab.id },
|
|
95
|
+
func: contentScriptHandler,
|
|
96
|
+
args: [ops],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const arr = results[0]?.result;
|
|
100
|
+
if (!arr) throw new Error("Batch content script returned no result");
|
|
101
|
+
return { results: arr };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// =========================================================================
|
|
105
|
+
// Navigation commands
|
|
106
|
+
// =========================================================================
|
|
107
|
+
|
|
108
|
+
export async function doNavigate(params) {
|
|
109
|
+
const url = params.url;
|
|
110
|
+
if (!url) throw new Error("Missing 'url' parameter");
|
|
111
|
+
const tab = await activeTab();
|
|
112
|
+
await chrome.tabs.update(tab.id, { url });
|
|
113
|
+
await waitForTabLoad(tab.id);
|
|
114
|
+
const updated = await chrome.tabs.get(tab.id);
|
|
115
|
+
return { url: updated.url, title: updated.title };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function doBack() {
|
|
119
|
+
const tab = await activeTab();
|
|
120
|
+
await chrome.tabs.goBack(tab.id);
|
|
121
|
+
await waitForTabLoad(tab.id);
|
|
122
|
+
const updated = await chrome.tabs.get(tab.id);
|
|
123
|
+
return { url: updated.url, title: updated.title };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function doForward() {
|
|
127
|
+
const tab = await activeTab();
|
|
128
|
+
await chrome.tabs.goForward(tab.id);
|
|
129
|
+
await waitForTabLoad(tab.id);
|
|
130
|
+
const updated = await chrome.tabs.get(tab.id);
|
|
131
|
+
return { url: updated.url, title: updated.title };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function doReload() {
|
|
135
|
+
const tab = await activeTab();
|
|
136
|
+
await chrome.tabs.reload(tab.id);
|
|
137
|
+
await waitForTabLoad(tab.id);
|
|
138
|
+
const updated = await chrome.tabs.get(tab.id);
|
|
139
|
+
return { url: updated.url, title: updated.title };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// =========================================================================
|
|
143
|
+
// Tab commands
|
|
144
|
+
// =========================================================================
|
|
145
|
+
|
|
146
|
+
export async function doTabs() {
|
|
147
|
+
const tabs = await chrome.tabs.query({});
|
|
148
|
+
const focusedWindow = await chrome.windows.getLastFocused();
|
|
149
|
+
return {
|
|
150
|
+
tabs: tabs.map((t) => ({
|
|
151
|
+
id: t.id,
|
|
152
|
+
url: t.url,
|
|
153
|
+
title: t.title,
|
|
154
|
+
active: t.active,
|
|
155
|
+
windowId: t.windowId,
|
|
156
|
+
})),
|
|
157
|
+
focusedWindowId: focusedWindow.id,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function doSwitchTab(params) {
|
|
162
|
+
const tabId = parseInt(params.id, 10);
|
|
163
|
+
if (isNaN(tabId)) throw new Error("Missing or invalid 'id' parameter");
|
|
164
|
+
const tab = await chrome.tabs.get(tabId);
|
|
165
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
166
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
167
|
+
const updated = await chrome.tabs.get(tabId);
|
|
168
|
+
return {
|
|
169
|
+
id: updated.id,
|
|
170
|
+
url: updated.url,
|
|
171
|
+
title: updated.title,
|
|
172
|
+
windowId: updated.windowId,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function doNewTab(params) {
|
|
177
|
+
const tab = await chrome.tabs.create({ url: params.url || "about:blank" });
|
|
178
|
+
if (params.url) await waitForTabLoad(tab.id);
|
|
179
|
+
const updated = await chrome.tabs.get(tab.id);
|
|
180
|
+
return { id: updated.id, url: updated.url, title: updated.title };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function doCloseTab(params) {
|
|
184
|
+
const tabId = params.id ? parseInt(params.id, 10) : (await activeTab()).id;
|
|
185
|
+
await chrome.tabs.remove(tabId);
|
|
186
|
+
return { closed: tabId };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =========================================================================
|
|
190
|
+
// Status / Screenshot
|
|
191
|
+
// =========================================================================
|
|
192
|
+
|
|
193
|
+
export async function doStatus() {
|
|
194
|
+
const tab = await activeTab();
|
|
195
|
+
return { url: tab.url, title: tab.title, id: tab.id };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function doScreenshot() {
|
|
199
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: "png" });
|
|
200
|
+
const base64 = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
201
|
+
return { format: "png", base64 };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// =========================================================================
|
|
205
|
+
// Download
|
|
206
|
+
// =========================================================================
|
|
207
|
+
|
|
208
|
+
export async function doDownload(params) {
|
|
209
|
+
const { url, selector, filename, index } = params;
|
|
210
|
+
|
|
211
|
+
let downloadUrl = url;
|
|
212
|
+
|
|
213
|
+
if (selector && !url) {
|
|
214
|
+
const result = await runInPage("extractUrl", { selector, index });
|
|
215
|
+
downloadUrl = result.url;
|
|
216
|
+
if (!downloadUrl)
|
|
217
|
+
throw new Error(`No downloadable URL found on element: ${selector}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!downloadUrl)
|
|
221
|
+
throw new Error("Missing 'url' or 'selector' parameter");
|
|
222
|
+
|
|
223
|
+
const downloadId = await new Promise((resolve, reject) => {
|
|
224
|
+
chrome.downloads.download(
|
|
225
|
+
{
|
|
226
|
+
url: downloadUrl,
|
|
227
|
+
filename: filename || undefined,
|
|
228
|
+
conflictAction: "uniquify",
|
|
229
|
+
},
|
|
230
|
+
(id) => {
|
|
231
|
+
if (chrome.runtime.lastError) {
|
|
232
|
+
reject(new Error(chrome.runtime.lastError.message));
|
|
233
|
+
} else {
|
|
234
|
+
resolve(id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const info = await waitForDownload(downloadId);
|
|
241
|
+
return info;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function waitForDownload(downloadId, timeoutMs = 30000) {
|
|
245
|
+
return new Promise((resolve) => {
|
|
246
|
+
const timer = setTimeout(() => {
|
|
247
|
+
chrome.downloads.onChanged.removeListener(listener);
|
|
248
|
+
resolve({ downloadId, state: "timeout" });
|
|
249
|
+
}, timeoutMs);
|
|
250
|
+
|
|
251
|
+
function listener(delta) {
|
|
252
|
+
if (delta.id !== downloadId) return;
|
|
253
|
+
if (delta.state && delta.state.current === "complete") {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
chrome.downloads.onChanged.removeListener(listener);
|
|
256
|
+
chrome.downloads.search({ id: downloadId }, (results) => {
|
|
257
|
+
const item = results[0];
|
|
258
|
+
resolve({
|
|
259
|
+
downloadId,
|
|
260
|
+
state: "complete",
|
|
261
|
+
filename: item?.filename,
|
|
262
|
+
fileSize: item?.fileSize,
|
|
263
|
+
mime: item?.mime,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
} else if (delta.state && delta.state.current === "interrupted") {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
chrome.downloads.onChanged.removeListener(listener);
|
|
269
|
+
resolve({
|
|
270
|
+
downloadId,
|
|
271
|
+
state: "failed",
|
|
272
|
+
error: delta.error?.current,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
chrome.downloads.onChanged.addListener(listener);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// =========================================================================
|
|
281
|
+
// Press key
|
|
282
|
+
// =========================================================================
|
|
283
|
+
|
|
284
|
+
export async function doPress(params) {
|
|
285
|
+
const key = params.key;
|
|
286
|
+
if (!key) throw new Error("Missing 'key' parameter");
|
|
287
|
+
return await runInPage("press", { key });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// =========================================================================
|
|
291
|
+
// Eval (script injection with CDP fallback)
|
|
292
|
+
// =========================================================================
|
|
293
|
+
|
|
294
|
+
export async function doEval(params) {
|
|
295
|
+
const code = params.code;
|
|
296
|
+
if (!code) throw new Error("Missing 'code' parameter");
|
|
297
|
+
const tab = await activeTab();
|
|
298
|
+
|
|
299
|
+
// Strategy 1: MAIN world <script> tag injection (fast path)
|
|
300
|
+
const results = await chrome.scripting.executeScript({
|
|
301
|
+
target: { tabId: tab.id },
|
|
302
|
+
func: (userCode) => {
|
|
303
|
+
const key = "__bctl_r_" + Math.random().toString(36).slice(2);
|
|
304
|
+
const script = document.createElement("script");
|
|
305
|
+
script.textContent =
|
|
306
|
+
"try{window['" +
|
|
307
|
+
key +
|
|
308
|
+
"']={v:(0,eval)(" +
|
|
309
|
+
JSON.stringify(userCode) +
|
|
310
|
+
")}}" +
|
|
311
|
+
"catch(e){window['" +
|
|
312
|
+
key +
|
|
313
|
+
"']={e:e.message||String(e)}}";
|
|
314
|
+
(document.head || document.documentElement).appendChild(script);
|
|
315
|
+
script.remove();
|
|
316
|
+
const r = window[key];
|
|
317
|
+
delete window[key];
|
|
318
|
+
if (!r) return null; // CSP blocked — fall through to debugger
|
|
319
|
+
if (r.e !== undefined) return { error: r.e };
|
|
320
|
+
return { value: r.v, ok: true };
|
|
321
|
+
},
|
|
322
|
+
args: [code],
|
|
323
|
+
world: "MAIN",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const r = results[0]?.result;
|
|
327
|
+
if (r && r.ok) return { result: r.value ?? null };
|
|
328
|
+
if (r && r.error) throw new Error(r.error);
|
|
329
|
+
|
|
330
|
+
// Strategy 2: Chrome DevTools Protocol via chrome.debugger (CSP fallback)
|
|
331
|
+
return await evalViaDebugger(tab.id, code);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function evalViaDebugger(tabId, code) {
|
|
335
|
+
try {
|
|
336
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
337
|
+
} catch (e) {
|
|
338
|
+
if (e.message?.includes("Another debugger")) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
"eval: cannot attach debugger (DevTools may be open). Close DevTools and retry, or use 'bctl select'/'bctl text' for DOM queries."
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
throw new Error("eval: cannot attach debugger — " + (e.message || e));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const result = await chrome.debugger.sendCommand(
|
|
348
|
+
{ tabId },
|
|
349
|
+
"Runtime.evaluate",
|
|
350
|
+
{ expression: code, returnByValue: true, awaitPromise: false }
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (result.exceptionDetails) {
|
|
354
|
+
const desc =
|
|
355
|
+
result.exceptionDetails.exception?.description ||
|
|
356
|
+
result.exceptionDetails.text ||
|
|
357
|
+
"Evaluation failed";
|
|
358
|
+
throw new Error(desc);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return { result: result.result?.value ?? null };
|
|
362
|
+
} finally {
|
|
363
|
+
try {
|
|
364
|
+
await chrome.debugger.detach({ tabId });
|
|
365
|
+
} catch (_) {
|
|
366
|
+
// ignore detach errors
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// =========================================================================
|
|
372
|
+
// Wait
|
|
373
|
+
// =========================================================================
|
|
374
|
+
|
|
375
|
+
export async function doWait(params) {
|
|
376
|
+
const selector = params.selector;
|
|
377
|
+
const timeout = params.timeout ?? 5;
|
|
378
|
+
|
|
379
|
+
if (!selector) {
|
|
380
|
+
const seconds = parseFloat(params.seconds ?? params.selector ?? timeout);
|
|
381
|
+
await new Promise((r) => setTimeout(r, seconds * 1000));
|
|
382
|
+
return { waited: seconds };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return await runInPage("wait", { selector, timeout });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// =========================================================================
|
|
389
|
+
// Upload (via Chrome DevTools Protocol)
|
|
390
|
+
// =========================================================================
|
|
391
|
+
|
|
392
|
+
export async function doUpload(params) {
|
|
393
|
+
const { selector, files } = params;
|
|
394
|
+
if (!selector) throw new Error("Missing 'selector' parameter");
|
|
395
|
+
if (!files || !files.length) throw new Error("Missing 'files' parameter");
|
|
396
|
+
|
|
397
|
+
const tab = await activeTab();
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
await chrome.debugger.attach({ tabId: tab.id }, "1.3");
|
|
401
|
+
} catch (e) {
|
|
402
|
+
if (e.message?.includes("Another debugger")) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
"upload: cannot attach debugger (DevTools may be open). Close DevTools and retry."
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
throw new Error("upload: cannot attach debugger — " + (e.message || e));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const doc = await chrome.debugger.sendCommand(
|
|
412
|
+
{ tabId: tab.id },
|
|
413
|
+
"DOM.getDocument",
|
|
414
|
+
{}
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const nodeResult = await chrome.debugger.sendCommand(
|
|
418
|
+
{ tabId: tab.id },
|
|
419
|
+
"DOM.querySelector",
|
|
420
|
+
{ nodeId: doc.root.nodeId, selector }
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
if (!nodeResult.nodeId) {
|
|
424
|
+
throw new Error(`Element not found: ${selector}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
await chrome.debugger.sendCommand(
|
|
428
|
+
{ tabId: tab.id },
|
|
429
|
+
"DOM.setFileInputFiles",
|
|
430
|
+
{ files, nodeId: nodeResult.nodeId }
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
return { uploaded: files.length, files, selector };
|
|
434
|
+
} finally {
|
|
435
|
+
try {
|
|
436
|
+
await chrome.debugger.detach({ tabId: tab.id });
|
|
437
|
+
} catch (_) {
|
|
438
|
+
// ignore detach errors
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// =========================================================================
|
|
444
|
+
// Dialog handling
|
|
445
|
+
// =========================================================================
|
|
446
|
+
|
|
447
|
+
export async function doDialog(params) {
|
|
448
|
+
const accept = params.accept !== false;
|
|
449
|
+
const text = params.text || "";
|
|
450
|
+
|
|
451
|
+
const tab = await activeTab();
|
|
452
|
+
|
|
453
|
+
await chrome.scripting.executeScript({
|
|
454
|
+
target: { tabId: tab.id },
|
|
455
|
+
func: (shouldAccept, responseText) => {
|
|
456
|
+
const origAlert = window.alert;
|
|
457
|
+
const origConfirm = window.confirm;
|
|
458
|
+
const origPrompt = window.prompt;
|
|
459
|
+
|
|
460
|
+
function restore() {
|
|
461
|
+
window.alert = origAlert;
|
|
462
|
+
window.confirm = origConfirm;
|
|
463
|
+
window.prompt = origPrompt;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
window.alert = function (message) {
|
|
467
|
+
window.__bctl_last_dialog = {
|
|
468
|
+
type: "alert",
|
|
469
|
+
message: String(message),
|
|
470
|
+
};
|
|
471
|
+
restore();
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
window.confirm = function (message) {
|
|
475
|
+
window.__bctl_last_dialog = {
|
|
476
|
+
type: "confirm",
|
|
477
|
+
message: String(message),
|
|
478
|
+
returned: shouldAccept,
|
|
479
|
+
};
|
|
480
|
+
restore();
|
|
481
|
+
return shouldAccept;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
window.prompt = function (message, defaultValue) {
|
|
485
|
+
const value = shouldAccept
|
|
486
|
+
? responseText || defaultValue || ""
|
|
487
|
+
: null;
|
|
488
|
+
window.__bctl_last_dialog = {
|
|
489
|
+
type: "prompt",
|
|
490
|
+
message: String(message),
|
|
491
|
+
returned: value,
|
|
492
|
+
};
|
|
493
|
+
restore();
|
|
494
|
+
return value;
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
args: [accept, text],
|
|
498
|
+
world: "MAIN",
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return { handler: accept ? "accept" : "dismiss", text: text || null };
|
|
502
|
+
}
|