poke-browser 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/cli.mjs +457 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +23 -0
- package/dist/logger.js.map +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +279 -0
- package/dist/run.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +13 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +707 -0
- package/dist/tools.js.map +1 -0
- package/dist/transport.d.ts +60 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +387 -0
- package/dist/transport.js.map +1 -0
- package/extension/background.js +1998 -0
- package/extension/content.js +1416 -0
- package/extension/icon.png +0 -0
- package/extension/manifest.json +48 -0
- package/extension/offscreen.html +10 -0
- package/extension/offscreen.js +246 -0
- package/extension/popup.html +426 -0
- package/extension/popup.js +205 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1998 @@
|
|
|
1
|
+
/** @typedef {{ type: string, requestId?: string, command?: string, payload?: unknown }} WsInbound */
|
|
2
|
+
|
|
3
|
+
const DEFAULT_WS_PORT = 9009;
|
|
4
|
+
const LOG_MAX = 50;
|
|
5
|
+
const NAVIGATE_WAIT_MS = 30_000;
|
|
6
|
+
/** CDP mouseMoved → wait → click so hover menus/tooltips can settle. */
|
|
7
|
+
const CLICK_ELEMENT_HOVER_DELAY_MS = 1000;
|
|
8
|
+
const MAX_NET_PER_TAB = 200;
|
|
9
|
+
|
|
10
|
+
/** @type {Set<number>} */
|
|
11
|
+
const networkCaptureTabs = new Set();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Per-tab network log: FIFO order of requestIds and merged entries.
|
|
15
|
+
* @type {Map<number, { order: string[]; byId: Map<string, Record<string, unknown>> }>}
|
|
16
|
+
*/
|
|
17
|
+
const networkStateByTab = new Map();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Long-lived port from offscreen.js (holds WebSocket). MV3 service workers cannot keep a socket
|
|
21
|
+
* across suspension; the offscreen document owns the connection.
|
|
22
|
+
* @type {chrome.runtime.Port | null}
|
|
23
|
+
*/
|
|
24
|
+
let bridgePort = null;
|
|
25
|
+
|
|
26
|
+
/** @type {"disconnected" | "connecting" | "connected"} */
|
|
27
|
+
let mcpStatus = "disconnected";
|
|
28
|
+
|
|
29
|
+
const KEEPALIVE_ALARM = "keepAlive";
|
|
30
|
+
|
|
31
|
+
/** In-flight offscreen creation; assigned synchronously when the inner async IIFE hits its first `await`. */
|
|
32
|
+
let offscreenCreatingPromise = null;
|
|
33
|
+
|
|
34
|
+
/** @type {Array<{ ts: number, direction: "in" | "out", summary: string }>} */
|
|
35
|
+
let commandLog = [];
|
|
36
|
+
|
|
37
|
+
function logCommand(direction, summary) {
|
|
38
|
+
commandLog.unshift({ ts: Date.now(), direction, summary });
|
|
39
|
+
if (commandLog.length > LOG_MAX) commandLog.length = LOG_MAX;
|
|
40
|
+
try {
|
|
41
|
+
if (chrome.runtime?.id) {
|
|
42
|
+
const t = new Date();
|
|
43
|
+
const stamp = t.toLocaleTimeString(undefined, { hour12: false });
|
|
44
|
+
const arrow = direction === "in" ? "←" : "→";
|
|
45
|
+
chrome.runtime
|
|
46
|
+
.sendMessage({
|
|
47
|
+
type: "log",
|
|
48
|
+
message: `[${stamp}] ${arrow} ${summary}`,
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {});
|
|
51
|
+
chrome.runtime.sendMessage({ type: "POKE_LOG_UPDATE" }).catch(() => {});
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
/* extension context invalidated */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getWsPort() {
|
|
59
|
+
const { wsPort } = await chrome.storage.local.get("wsPort");
|
|
60
|
+
if (typeof wsPort === "number" && Number.isFinite(wsPort) && wsPort > 0 && wsPort < 65536) {
|
|
61
|
+
return wsPort;
|
|
62
|
+
}
|
|
63
|
+
return DEFAULT_WS_PORT;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function getPokeEnabled() {
|
|
67
|
+
const { enabled } = await chrome.storage.local.get("enabled");
|
|
68
|
+
if (typeof enabled === "boolean") return enabled;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function stopMcpConnection() {
|
|
73
|
+
try {
|
|
74
|
+
if (await chrome.offscreen.hasDocument()) {
|
|
75
|
+
await chrome.offscreen.closeDocument();
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error("[poke-browser ext] closeDocument failed:", e);
|
|
79
|
+
}
|
|
80
|
+
bridgePort = null;
|
|
81
|
+
setStatus("disconnected");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function getWsAuthToken() {
|
|
85
|
+
const { wsAuthToken } = await chrome.storage.local.get("wsAuthToken");
|
|
86
|
+
return typeof wsAuthToken === "string" ? wsAuthToken : "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setStatus(next) {
|
|
90
|
+
mcpStatus = next;
|
|
91
|
+
try {
|
|
92
|
+
if (chrome.runtime?.id) {
|
|
93
|
+
chrome.runtime.sendMessage({ type: "POKE_STATUS", status: next }).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
/* extension context invalidated */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Idempotent offscreen creation: concurrent onInstalled / onStartup / alarm / SW wake all await the same work.
|
|
102
|
+
*/
|
|
103
|
+
async function setupOffscreen() {
|
|
104
|
+
if (offscreenCreatingPromise) {
|
|
105
|
+
return offscreenCreatingPromise;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
offscreenCreatingPromise = (async () => {
|
|
109
|
+
try {
|
|
110
|
+
try {
|
|
111
|
+
if (await chrome.offscreen.hasDocument()) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error("[poke-browser ext] hasDocument check failed:", e);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const stored = await chrome.storage.local.get(["wsPort", "wsUrl"]);
|
|
119
|
+
const storedWsUrl =
|
|
120
|
+
typeof stored.wsUrl === "string" && stored.wsUrl.trim() ? stored.wsUrl.trim() : "";
|
|
121
|
+
const raw = stored.wsPort;
|
|
122
|
+
const port =
|
|
123
|
+
typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 65536
|
|
124
|
+
? Math.trunc(raw)
|
|
125
|
+
: DEFAULT_WS_PORT;
|
|
126
|
+
|
|
127
|
+
const docUrl = new URL(chrome.runtime.getURL("offscreen.html"));
|
|
128
|
+
if (storedWsUrl) {
|
|
129
|
+
docUrl.searchParams.set("wsUrl", storedWsUrl);
|
|
130
|
+
} else {
|
|
131
|
+
docUrl.searchParams.set("port", String(port));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await chrome.offscreen.createDocument({
|
|
136
|
+
url: docUrl.href,
|
|
137
|
+
reasons: [chrome.offscreen.Reason.DOM_SCRAPING],
|
|
138
|
+
justification:
|
|
139
|
+
"Maintain persistent WebSocket connection to poke-browser MCP server for browser automation",
|
|
140
|
+
});
|
|
141
|
+
console.log("[poke-browser ext] Offscreen document created");
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const msg =
|
|
144
|
+
err && typeof err === "object" && "message" in err
|
|
145
|
+
? String(/** @type {{ message?: string }} */ (err).message)
|
|
146
|
+
: String(err);
|
|
147
|
+
if (msg.includes("single offscreen document") || msg.includes("already exists")) {
|
|
148
|
+
console.log(
|
|
149
|
+
"[poke-browser ext] Offscreen already exists (concurrent creation), ignoring",
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
console.error("[poke-browser ext] Failed to create offscreen document:", err);
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
offscreenCreatingPromise = null;
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
return offscreenCreatingPromise;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function scheduleKeepAliveAlarm() {
|
|
165
|
+
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.4 });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
169
|
+
if (alarm.name !== KEEPALIVE_ALARM) return;
|
|
170
|
+
void (async () => {
|
|
171
|
+
if (!(await getPokeEnabled())) return;
|
|
172
|
+
try {
|
|
173
|
+
await setupOffscreen();
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.error("[poke-browser ext] setupOffscreen failed:", e);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
bridgePort?.postMessage({ type: "sw_wake" });
|
|
179
|
+
} catch {
|
|
180
|
+
/* ignore */
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
async function ensureOffscreenAndSchedule() {
|
|
186
|
+
if (!(await getPokeEnabled())) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
await setupOffscreen();
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error("[poke-browser ext] setupOffscreen failed:", e);
|
|
193
|
+
}
|
|
194
|
+
scheduleKeepAliveAlarm();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
chrome.runtime.onConnect.addListener((port) => {
|
|
198
|
+
if (port.name !== "POKE_WS_BRIDGE") return;
|
|
199
|
+
bridgePort = port;
|
|
200
|
+
console.log("[poke-browser ext] Offscreen bridge port connected");
|
|
201
|
+
port.onMessage.addListener((msg) => {
|
|
202
|
+
if (msg && typeof msg === "object" && msg.type === "request_hello_credentials") {
|
|
203
|
+
void getWsAuthToken().then((token) => {
|
|
204
|
+
try {
|
|
205
|
+
port.postMessage({
|
|
206
|
+
type: "hello_credentials",
|
|
207
|
+
token,
|
|
208
|
+
version: chrome.runtime.getManifest().version,
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
/* ignore */
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (msg && typeof msg === "object" && msg.type === "ws_message" && typeof msg.data === "string") {
|
|
217
|
+
void handleSocketMessage(msg.data).catch((err) => {
|
|
218
|
+
logCommand("in", `Handler error: ${String(err)}`);
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (msg && typeof msg === "object" && msg.type === "ws_frame" && typeof msg.raw === "string") {
|
|
223
|
+
void handleSocketMessage(msg.raw).catch((err) => {
|
|
224
|
+
logCommand("in", `Handler error: ${String(err)}`);
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (msg && typeof msg === "object" && msg.type === "ws_connected") {
|
|
229
|
+
setStatus("connected");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (msg && typeof msg === "object" && msg.type === "ws_disconnected") {
|
|
233
|
+
setStatus("disconnected");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (msg && typeof msg === "object" && msg.type === "ws_status" && typeof msg.status === "string") {
|
|
237
|
+
setStatus(msg.status);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (
|
|
241
|
+
msg &&
|
|
242
|
+
typeof msg === "object" &&
|
|
243
|
+
msg.type === "ws_log" &&
|
|
244
|
+
typeof msg.direction === "string" &&
|
|
245
|
+
typeof msg.summary === "string"
|
|
246
|
+
) {
|
|
247
|
+
logCommand(msg.direction, msg.summary);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
port.onDisconnect.addListener(() => {
|
|
251
|
+
if (bridgePort === port) bridgePort = null;
|
|
252
|
+
console.log("[poke-browser ext] Offscreen bridge port disconnected");
|
|
253
|
+
setStatus("disconnected");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @param {unknown} data
|
|
259
|
+
*/
|
|
260
|
+
function safeSend(data) {
|
|
261
|
+
if (!bridgePort) return false;
|
|
262
|
+
try {
|
|
263
|
+
bridgePort.postMessage({ type: "ws_send", payload: data });
|
|
264
|
+
return true;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {string} raw
|
|
272
|
+
*/
|
|
273
|
+
async function handleSocketMessage(raw) {
|
|
274
|
+
/** @type {WsInbound} */
|
|
275
|
+
let msg;
|
|
276
|
+
try {
|
|
277
|
+
msg = JSON.parse(raw);
|
|
278
|
+
} catch {
|
|
279
|
+
logCommand("in", "Invalid JSON from MCP");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (msg.type === "auth_ok") {
|
|
284
|
+
console.log("[poke-browser ext] Auth OK received, connection fully established");
|
|
285
|
+
logCommand("out", "WebSocket: auth OK from MCP");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (msg.type !== "command" || typeof msg.requestId !== "string" || typeof msg.command !== "string") {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
logCommand("in", `${msg.command} (${msg.requestId.slice(0, 8)}…)`);
|
|
294
|
+
try {
|
|
295
|
+
const result = await dispatchCommand(msg.command, msg.payload);
|
|
296
|
+
safeSend({ type: "response", requestId: msg.requestId, ok: true, result });
|
|
297
|
+
logCommand("out", `OK ${msg.command}`);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
300
|
+
safeSend({ type: "response", requestId: msg.requestId, ok: false, error });
|
|
301
|
+
logCommand("out", `ERR ${msg.command}: ${error}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {unknown} payload
|
|
307
|
+
*/
|
|
308
|
+
function asPayload(payload) {
|
|
309
|
+
return /** @type {Record<string, unknown>} */ (payload && typeof payload === "object" ? payload : {});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @param {number | undefined} tabId
|
|
314
|
+
*/
|
|
315
|
+
async function resolveTabId(tabId) {
|
|
316
|
+
if (typeof tabId === "number" && Number.isFinite(tabId)) {
|
|
317
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
318
|
+
if (!tab) throw new Error(`Tab not found: ${tabId}`);
|
|
319
|
+
return tabId;
|
|
320
|
+
}
|
|
321
|
+
const [active] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
322
|
+
if (!active?.id) throw new Error("No active tab");
|
|
323
|
+
return active.id;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Merge Chrome tab metadata into tool results (tabId, url, title from tabs.get).
|
|
328
|
+
* @param {number} tabId
|
|
329
|
+
* @param {unknown} value
|
|
330
|
+
*/
|
|
331
|
+
async function withTabMeta(tabId, value) {
|
|
332
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
333
|
+
const meta = {
|
|
334
|
+
tabId,
|
|
335
|
+
url: tab?.url ?? "",
|
|
336
|
+
title: tab?.title ?? "",
|
|
337
|
+
};
|
|
338
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
339
|
+
return { ...(/** @type {Record<string, unknown>} */ (value)), ...meta };
|
|
340
|
+
}
|
|
341
|
+
return { ...meta, value };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Bring a tab to the foreground so captureVisibleTab targets it.
|
|
346
|
+
* @param {number} tabId
|
|
347
|
+
*/
|
|
348
|
+
async function ensureTabVisibleForCapture(tabId) {
|
|
349
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
350
|
+
if (!tab?.id || tab.windowId == null) throw new Error(`Tab not found: ${tabId}`);
|
|
351
|
+
if (!tab.active) {
|
|
352
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
353
|
+
}
|
|
354
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
355
|
+
await new Promise((r) => setTimeout(r, 75));
|
|
356
|
+
return tab;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {number} tabId
|
|
361
|
+
* @param {string} method
|
|
362
|
+
* @param {object} [params]
|
|
363
|
+
*/
|
|
364
|
+
function debuggerSend(tabId, method, params = {}) {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
chrome.debugger.sendCommand({ tabId }, method, params, () => {
|
|
367
|
+
const err = chrome.runtime.lastError;
|
|
368
|
+
if (err) reject(new Error(err.message));
|
|
369
|
+
else resolve(undefined);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @param {number} tabId
|
|
376
|
+
* @param {string} method
|
|
377
|
+
* @param {object} [params]
|
|
378
|
+
* @returns {Promise<unknown>}
|
|
379
|
+
*/
|
|
380
|
+
function debuggerSendWithResult(tabId, method, params = {}) {
|
|
381
|
+
return new Promise((resolve, reject) => {
|
|
382
|
+
chrome.debugger.sendCommand({ tabId }, method, params, (result) => {
|
|
383
|
+
const err = chrome.runtime.lastError;
|
|
384
|
+
if (err) reject(new Error(err.message));
|
|
385
|
+
else resolve(result);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {number} tabId
|
|
392
|
+
*/
|
|
393
|
+
async function debuggerAttach(tabId) {
|
|
394
|
+
await new Promise((resolve, reject) => {
|
|
395
|
+
chrome.debugger.attach({ tabId }, "1.3", () => {
|
|
396
|
+
const err = chrome.runtime.lastError;
|
|
397
|
+
if (err) reject(new Error(err.message));
|
|
398
|
+
else resolve(undefined);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* @param {number} tabId
|
|
405
|
+
*/
|
|
406
|
+
async function debuggerDetach(tabId) {
|
|
407
|
+
await new Promise((resolve) => {
|
|
408
|
+
chrome.debugger.detach({ tabId }, () => resolve());
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @param {number} tabId
|
|
414
|
+
*/
|
|
415
|
+
function isNetworkCapturing(tabId) {
|
|
416
|
+
return networkCaptureTabs.has(tabId);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @param {number} tabId
|
|
421
|
+
*/
|
|
422
|
+
async function debuggerAttachForTool(tabId) {
|
|
423
|
+
if (isNetworkCapturing(tabId)) return;
|
|
424
|
+
await debuggerAttach(tabId);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @param {number} tabId
|
|
429
|
+
*/
|
|
430
|
+
async function debuggerDetachForTool(tabId) {
|
|
431
|
+
if (isNetworkCapturing(tabId)) return;
|
|
432
|
+
await debuggerDetach(tabId);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* @param {unknown} headers
|
|
437
|
+
* @returns {Record<string, string>}
|
|
438
|
+
*/
|
|
439
|
+
function normalizeHeaders(headers) {
|
|
440
|
+
if (!headers || typeof headers !== "object") return {};
|
|
441
|
+
if (Array.isArray(headers)) {
|
|
442
|
+
/** @type {Record<string, string>} */
|
|
443
|
+
const o = {};
|
|
444
|
+
for (const row of headers) {
|
|
445
|
+
if (row && typeof row === "object" && "name" in row) {
|
|
446
|
+
const name = String(/** @type {{ name?: string }} */ (row).name ?? "");
|
|
447
|
+
if (name) o[name] = String(/** @type {{ value?: string }} */ (row).value ?? "");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return o;
|
|
451
|
+
}
|
|
452
|
+
/** @type {Record<string, string>} */
|
|
453
|
+
const out = {};
|
|
454
|
+
for (const [k, v] of Object.entries(/** @type {Record<string, unknown>} */ (headers))) {
|
|
455
|
+
out[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* @param {number} tabId
|
|
462
|
+
* @param {string} requestId
|
|
463
|
+
* @param {Record<string, unknown>} patch
|
|
464
|
+
*/
|
|
465
|
+
function upsertNetworkEntry(tabId, requestId, patch) {
|
|
466
|
+
let state = networkStateByTab.get(tabId);
|
|
467
|
+
if (!state) {
|
|
468
|
+
state = { order: [], byId: new Map() };
|
|
469
|
+
networkStateByTab.set(tabId, state);
|
|
470
|
+
}
|
|
471
|
+
const existing = state.byId.get(requestId);
|
|
472
|
+
if (existing) {
|
|
473
|
+
Object.assign(existing, patch);
|
|
474
|
+
} else {
|
|
475
|
+
state.byId.set(requestId, { requestId, ...patch });
|
|
476
|
+
state.order.push(requestId);
|
|
477
|
+
while (state.order.length > MAX_NET_PER_TAB) {
|
|
478
|
+
const drop = state.order.shift();
|
|
479
|
+
if (drop) state.byId.delete(drop);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
485
|
+
const tabId = source.tabId;
|
|
486
|
+
if (tabId == null || !networkCaptureTabs.has(tabId)) return;
|
|
487
|
+
const p = params && typeof params === "object" ? /** @type {Record<string, unknown>} */ (params) : {};
|
|
488
|
+
const rid = p.requestId != null ? String(p.requestId) : "";
|
|
489
|
+
if (!rid) return;
|
|
490
|
+
|
|
491
|
+
if (method === "Network.requestWillBeSent") {
|
|
492
|
+
const req = /** @type {{ url?: string; method?: string; headers?: unknown }} */ (p.request ?? {});
|
|
493
|
+
upsertNetworkEntry(tabId, rid, {
|
|
494
|
+
url: req.url ?? "",
|
|
495
|
+
method: req.method ?? "GET",
|
|
496
|
+
requestHeaders: normalizeHeaders(req.headers),
|
|
497
|
+
});
|
|
498
|
+
} else if (method === "Network.responseReceived") {
|
|
499
|
+
const res = /** @type {{ status?: number; mimeType?: string; headers?: unknown; timing?: unknown }} */ (p.response ?? {});
|
|
500
|
+
upsertNetworkEntry(tabId, rid, {
|
|
501
|
+
status: res.status,
|
|
502
|
+
mimeType: res.mimeType,
|
|
503
|
+
responseHeaders: normalizeHeaders(res.headers),
|
|
504
|
+
timing: res.timing ?? null,
|
|
505
|
+
});
|
|
506
|
+
} else if (method === "Network.loadingFinished") {
|
|
507
|
+
upsertNetworkEntry(tabId, rid, {
|
|
508
|
+
bodySize: typeof p.encodedDataLength === "number" ? p.encodedDataLength : undefined,
|
|
509
|
+
loaded: true,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
chrome.debugger.onDetach.addListener((source, _reason) => {
|
|
515
|
+
if (source.tabId != null) networkCaptureTabs.delete(source.tabId);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @param {number} tabId
|
|
520
|
+
* @param {number} x
|
|
521
|
+
* @param {number} y
|
|
522
|
+
*/
|
|
523
|
+
async function clickViaDebugger(tabId, x, y) {
|
|
524
|
+
await debuggerAttachForTool(tabId);
|
|
525
|
+
try {
|
|
526
|
+
await debuggerSend(tabId, "Input.dispatchMouseEvent", {
|
|
527
|
+
type: "mousePressed",
|
|
528
|
+
x,
|
|
529
|
+
y,
|
|
530
|
+
button: "left",
|
|
531
|
+
clickCount: 1,
|
|
532
|
+
});
|
|
533
|
+
await debuggerSend(tabId, "Input.dispatchMouseEvent", {
|
|
534
|
+
type: "mouseReleased",
|
|
535
|
+
x,
|
|
536
|
+
y,
|
|
537
|
+
button: "left",
|
|
538
|
+
clickCount: 1,
|
|
539
|
+
});
|
|
540
|
+
return { success: true };
|
|
541
|
+
} finally {
|
|
542
|
+
await debuggerDetachForTool(tabId);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Select-all then Backspace via CDP so the focused field is cleared before typing.
|
|
548
|
+
* @param {number} tabId
|
|
549
|
+
*/
|
|
550
|
+
async function clearFocusedFieldViaDebuggerKeys(tabId) {
|
|
551
|
+
const info = await chrome.runtime.getPlatformInfo();
|
|
552
|
+
const mod = info.os === "mac" ? 4 : 2;
|
|
553
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
554
|
+
type: "keyDown",
|
|
555
|
+
key: "a",
|
|
556
|
+
code: "KeyA",
|
|
557
|
+
windowsVirtualKeyCode: 65,
|
|
558
|
+
modifiers: mod,
|
|
559
|
+
});
|
|
560
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
561
|
+
type: "keyUp",
|
|
562
|
+
key: "a",
|
|
563
|
+
code: "KeyA",
|
|
564
|
+
windowsVirtualKeyCode: 65,
|
|
565
|
+
modifiers: mod,
|
|
566
|
+
});
|
|
567
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
568
|
+
type: "keyDown",
|
|
569
|
+
key: "Backspace",
|
|
570
|
+
code: "Backspace",
|
|
571
|
+
windowsVirtualKeyCode: 8,
|
|
572
|
+
});
|
|
573
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
574
|
+
type: "keyUp",
|
|
575
|
+
key: "Backspace",
|
|
576
|
+
code: "Backspace",
|
|
577
|
+
windowsVirtualKeyCode: 8,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @param {number} tabId
|
|
583
|
+
* @param {string} text
|
|
584
|
+
* @param {boolean} [clearField]
|
|
585
|
+
*/
|
|
586
|
+
async function typeTextViaDebugger(tabId, text, clearField) {
|
|
587
|
+
await debuggerAttachForTool(tabId);
|
|
588
|
+
try {
|
|
589
|
+
if (clearField) {
|
|
590
|
+
await clearFocusedFieldViaDebuggerKeys(tabId);
|
|
591
|
+
}
|
|
592
|
+
for (const ch of text) {
|
|
593
|
+
if (ch === "\n" || ch === "\r") {
|
|
594
|
+
if (ch === "\r") continue;
|
|
595
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
596
|
+
type: "keyDown",
|
|
597
|
+
key: "Enter",
|
|
598
|
+
code: "Enter",
|
|
599
|
+
windowsVirtualKeyCode: 13,
|
|
600
|
+
});
|
|
601
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
602
|
+
type: "keyUp",
|
|
603
|
+
key: "Enter",
|
|
604
|
+
code: "Enter",
|
|
605
|
+
windowsVirtualKeyCode: 13,
|
|
606
|
+
});
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
610
|
+
type: "keyDown",
|
|
611
|
+
text: ch,
|
|
612
|
+
});
|
|
613
|
+
await debuggerSend(tabId, "Input.dispatchKeyEvent", {
|
|
614
|
+
type: "keyUp",
|
|
615
|
+
text: ch,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return { success: true, charsTyped: text.length };
|
|
619
|
+
} finally {
|
|
620
|
+
await debuggerDetachForTool(tabId);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Temporary viewport dot for coordinate-based tools. Serialized into the page by chrome.scripting.
|
|
626
|
+
* @param {{ x?: unknown; y?: unknown }} p
|
|
627
|
+
*/
|
|
628
|
+
function pokeInjectedCursorFeedbackDot(p) {
|
|
629
|
+
const x = typeof p.x === "number" ? p.x : Number(p.x);
|
|
630
|
+
const y = typeof p.y === "number" ? p.y : Number(p.y);
|
|
631
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
632
|
+
|
|
633
|
+
const dot = document.createElement("div");
|
|
634
|
+
dot.setAttribute("data-poke-cursor-feedback", "1");
|
|
635
|
+
Object.assign(dot.style, {
|
|
636
|
+
position: "fixed",
|
|
637
|
+
left: `${x - 8}px`,
|
|
638
|
+
top: `${y - 8}px`,
|
|
639
|
+
width: "16px",
|
|
640
|
+
height: "16px",
|
|
641
|
+
borderRadius: "50%",
|
|
642
|
+
background: "rgb(255, 0, 0)",
|
|
643
|
+
zIndex: "999999",
|
|
644
|
+
pointerEvents: "none",
|
|
645
|
+
opacity: "1",
|
|
646
|
+
transition: "opacity 600ms ease-out",
|
|
647
|
+
boxSizing: "border-box",
|
|
648
|
+
});
|
|
649
|
+
(document.documentElement || document.body).appendChild(dot);
|
|
650
|
+
requestAnimationFrame(() => {
|
|
651
|
+
requestAnimationFrame(() => {
|
|
652
|
+
dot.style.opacity = "0";
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
setTimeout(() => dot.remove(), 650);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* @param {number} tabId
|
|
660
|
+
* @param {number} x
|
|
661
|
+
* @param {number} y
|
|
662
|
+
*/
|
|
663
|
+
async function showCursorFeedbackDot(tabId, x, y) {
|
|
664
|
+
try {
|
|
665
|
+
await chrome.scripting.executeScript({
|
|
666
|
+
target: { tabId, allFrames: false },
|
|
667
|
+
world: "MAIN",
|
|
668
|
+
injectImmediately: true,
|
|
669
|
+
func: pokeInjectedCursorFeedbackDot,
|
|
670
|
+
args: [{ x, y }],
|
|
671
|
+
});
|
|
672
|
+
} catch {
|
|
673
|
+
/* chrome:// and other restricted tabs — automation continues */
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* @param {number} tabId
|
|
679
|
+
* @param {number} timeoutMs
|
|
680
|
+
*/
|
|
681
|
+
function waitForTabLoadComplete(tabId, timeoutMs) {
|
|
682
|
+
return new Promise((resolve, reject) => {
|
|
683
|
+
const timer = setTimeout(() => {
|
|
684
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
685
|
+
reject(new Error("navigate_to: load timeout"));
|
|
686
|
+
}, timeoutMs);
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* @param {number} id
|
|
690
|
+
* @param {chrome.tabs.TabChangeInfo} changeInfo
|
|
691
|
+
*/
|
|
692
|
+
function onUpdated(id, changeInfo) {
|
|
693
|
+
if (id !== tabId) return;
|
|
694
|
+
if (changeInfo.status === "complete") {
|
|
695
|
+
clearTimeout(timer);
|
|
696
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
697
|
+
resolve();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function handleListTabs() {
|
|
706
|
+
const tabs = await chrome.tabs.query({});
|
|
707
|
+
return tabs
|
|
708
|
+
.filter((t) => t.id != null)
|
|
709
|
+
.map((t) => ({
|
|
710
|
+
tabId: t.id,
|
|
711
|
+
title: t.title ?? "",
|
|
712
|
+
url: t.url ?? "",
|
|
713
|
+
active: Boolean(t.active),
|
|
714
|
+
index: t.index,
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function handleGetActiveTab() {
|
|
719
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
720
|
+
if (!tab?.id) throw new Error("No active tab");
|
|
721
|
+
return {
|
|
722
|
+
tabId: tab.id,
|
|
723
|
+
title: tab.title ?? "",
|
|
724
|
+
url: tab.url ?? "",
|
|
725
|
+
active: true,
|
|
726
|
+
index: tab.index,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** @param {unknown} payload */
|
|
731
|
+
async function handleNavigateTo(payload) {
|
|
732
|
+
const p = asPayload(payload);
|
|
733
|
+
const url = typeof p.url === "string" ? p.url : "";
|
|
734
|
+
if (!url) throw new Error("navigate_to requires url");
|
|
735
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
736
|
+
/** Always wait for chrome.tabs.onUpdated status "complete" so finalUrl/title match the loaded page (not a stale devtools/interstitial URL). */
|
|
737
|
+
const timeoutMs = p.waitForLoad === false ? 10_000 : NAVIGATE_WAIT_MS;
|
|
738
|
+
const done = waitForTabLoadComplete(tabId, timeoutMs);
|
|
739
|
+
await chrome.tabs.update(tabId, { url });
|
|
740
|
+
await done;
|
|
741
|
+
const tab = await chrome.tabs.get(tabId);
|
|
742
|
+
const finalUrl = tab.url ?? "";
|
|
743
|
+
const title = tab.title ?? "";
|
|
744
|
+
return {
|
|
745
|
+
success: true,
|
|
746
|
+
tabId,
|
|
747
|
+
url: finalUrl,
|
|
748
|
+
finalUrl,
|
|
749
|
+
title,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** @param {unknown} payload */
|
|
754
|
+
async function handleClickElement(payload) {
|
|
755
|
+
const p = asPayload(payload);
|
|
756
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
757
|
+
const selector = typeof p.selector === "string" ? p.selector.trim() : "";
|
|
758
|
+
const x = typeof p.x === "number" ? p.x : Number(p.x);
|
|
759
|
+
const y = typeof p.y === "number" ? p.y : Number(p.y);
|
|
760
|
+
const hasXY = Number.isFinite(x) && Number.isFinite(y);
|
|
761
|
+
|
|
762
|
+
if (selector) {
|
|
763
|
+
const pt = await chrome.tabs
|
|
764
|
+
.sendMessage(tabId, { type: "POKE_RESOLVE_CLICK_POINT", selector })
|
|
765
|
+
.catch((e) => {
|
|
766
|
+
throw new Error(`click_element resolve failed: ${String(e)}`);
|
|
767
|
+
});
|
|
768
|
+
if (
|
|
769
|
+
!pt ||
|
|
770
|
+
pt.success !== true ||
|
|
771
|
+
typeof pt.x !== "number" ||
|
|
772
|
+
typeof pt.y !== "number" ||
|
|
773
|
+
!Number.isFinite(pt.x) ||
|
|
774
|
+
!Number.isFinite(pt.y)
|
|
775
|
+
) {
|
|
776
|
+
const err = pt && typeof pt.error === "string" ? pt.error : "could not resolve target coordinates";
|
|
777
|
+
throw new Error(`click_element ${err}`);
|
|
778
|
+
}
|
|
779
|
+
await showCursorFeedbackDot(tabId, pt.x, pt.y);
|
|
780
|
+
await debuggerAttachForTool(tabId);
|
|
781
|
+
try {
|
|
782
|
+
await debuggerSend(tabId, "Input.dispatchMouseEvent", {
|
|
783
|
+
type: "mouseMoved",
|
|
784
|
+
x: pt.x,
|
|
785
|
+
y: pt.y,
|
|
786
|
+
});
|
|
787
|
+
await new Promise((r) => setTimeout(r, CLICK_ELEMENT_HOVER_DELAY_MS));
|
|
788
|
+
} finally {
|
|
789
|
+
await debuggerDetachForTool(tabId);
|
|
790
|
+
}
|
|
791
|
+
const res = await chrome.tabs.sendMessage(tabId, { type: "POKE_CLICK_ELEMENT", selector }).catch((e) => {
|
|
792
|
+
throw new Error(`click_element relay failed: ${String(e)}`);
|
|
793
|
+
});
|
|
794
|
+
return withTabMeta(tabId, res);
|
|
795
|
+
}
|
|
796
|
+
if (hasXY) {
|
|
797
|
+
await showCursorFeedbackDot(tabId, x, y);
|
|
798
|
+
const r = await clickViaDebugger(tabId, x, y);
|
|
799
|
+
return withTabMeta(tabId, r);
|
|
800
|
+
}
|
|
801
|
+
throw new Error("click_element requires selector or numeric x and y");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** @param {unknown} payload */
|
|
805
|
+
async function handleTypeText(payload) {
|
|
806
|
+
const p = asPayload(payload);
|
|
807
|
+
const text = typeof p.text === "string" ? p.text : "";
|
|
808
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
809
|
+
const selector = typeof p.selector === "string" ? p.selector : undefined;
|
|
810
|
+
const shouldClear = p.clear !== false;
|
|
811
|
+
const tx = typeof p.x === "number" ? p.x : Number(p.x);
|
|
812
|
+
const ty = typeof p.y === "number" ? p.y : Number(p.y);
|
|
813
|
+
const hasXY = Number.isFinite(tx) && Number.isFinite(ty);
|
|
814
|
+
if (hasXY) await showCursorFeedbackDot(tabId, tx, ty);
|
|
815
|
+
|
|
816
|
+
const res = await chrome.tabs
|
|
817
|
+
.sendMessage(tabId, {
|
|
818
|
+
type: "POKE_TYPE_TEXT",
|
|
819
|
+
text,
|
|
820
|
+
selector,
|
|
821
|
+
clear: shouldClear,
|
|
822
|
+
})
|
|
823
|
+
.catch(() => null);
|
|
824
|
+
|
|
825
|
+
if (res && res.success === true) {
|
|
826
|
+
return withTabMeta(tabId, {
|
|
827
|
+
success: true,
|
|
828
|
+
charsTyped: typeof res.charsTyped === "number" ? res.charsTyped : text.length,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const dbg = await typeTextViaDebugger(tabId, text, shouldClear);
|
|
832
|
+
return withTabMeta(tabId, dbg);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Main-world scroll implementation injected via chrome.scripting (guarantees the target tab frame).
|
|
837
|
+
* Must be self-contained — Chrome serializes this function into the page.
|
|
838
|
+
* @param {Record<string, unknown>} p
|
|
839
|
+
*/
|
|
840
|
+
function pokeInjectedScrollWindow(p) {
|
|
841
|
+
const behavior = p.behavior === "smooth" ? "smooth" : "auto";
|
|
842
|
+
const selector = typeof p.selector === "string" ? p.selector.trim() : "";
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* @param {string} s
|
|
846
|
+
* @returns {Element | null}
|
|
847
|
+
*/
|
|
848
|
+
function querySelectorOrXPath(s) {
|
|
849
|
+
const t = s.trim();
|
|
850
|
+
if (t.startsWith("//") || t.toLowerCase().startsWith("xpath:")) {
|
|
851
|
+
const expr = t.toLowerCase().startsWith("xpath:") ? t.slice(6).trim() : t;
|
|
852
|
+
try {
|
|
853
|
+
const r = document.evaluate(expr, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
854
|
+
const node = r.singleNodeValue;
|
|
855
|
+
return node instanceof Element ? node : null;
|
|
856
|
+
} catch {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
return document.querySelector(t);
|
|
862
|
+
} catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const dirRaw = typeof p.direction === "string" ? p.direction.toLowerCase() : "";
|
|
868
|
+
const dir =
|
|
869
|
+
dirRaw === "up" || dirRaw === "down" || dirRaw === "left" || dirRaw === "right" ? dirRaw : "";
|
|
870
|
+
|
|
871
|
+
try {
|
|
872
|
+
if (selector) {
|
|
873
|
+
const el = querySelectorOrXPath(selector);
|
|
874
|
+
if (!el) {
|
|
875
|
+
return { success: false, scrollX: window.scrollX, scrollY: window.scrollY, error: "Element not found" };
|
|
876
|
+
}
|
|
877
|
+
el.scrollIntoView({ behavior, block: "center", inline: "nearest" });
|
|
878
|
+
return { success: true, scrollX: window.scrollX, scrollY: window.scrollY };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (typeof p.x === "number" || typeof p.y === "number") {
|
|
882
|
+
const left = typeof p.x === "number" ? p.x : window.scrollX;
|
|
883
|
+
const top = typeof p.y === "number" ? p.y : window.scrollY;
|
|
884
|
+
window.scrollTo({ left, top, behavior });
|
|
885
|
+
return { success: true, scrollX: window.scrollX, scrollY: window.scrollY };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let dx = typeof p.deltaX === "number" && Number.isFinite(p.deltaX) ? p.deltaX : 0;
|
|
889
|
+
let dy = typeof p.deltaY === "number" && Number.isFinite(p.deltaY) ? p.deltaY : 0;
|
|
890
|
+
|
|
891
|
+
if (dir) {
|
|
892
|
+
let amt = typeof p.amount === "number" && Number.isFinite(p.amount) ? Math.abs(p.amount) : NaN;
|
|
893
|
+
if (!Number.isFinite(amt) || amt === 0) {
|
|
894
|
+
if (dir === "up" || dir === "down") {
|
|
895
|
+
const fromDelta = typeof p.deltaY === "number" && Number.isFinite(p.deltaY) && p.deltaY !== 0;
|
|
896
|
+
amt = fromDelta ? Math.abs(p.deltaY) : Math.max(200, Math.floor(window.innerHeight * 0.85));
|
|
897
|
+
} else {
|
|
898
|
+
const fromDelta = typeof p.deltaX === "number" && Number.isFinite(p.deltaX) && p.deltaX !== 0;
|
|
899
|
+
amt = fromDelta ? Math.abs(p.deltaX) : Math.max(200, Math.floor(window.innerWidth * 0.85));
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
dx = dir === "left" ? -amt : dir === "right" ? amt : 0;
|
|
903
|
+
dy = dir === "up" ? -amt : dir === "down" ? amt : 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
window.scrollBy({ left: dx, top: dy, behavior });
|
|
907
|
+
return { success: true, scrollX: window.scrollX, scrollY: window.scrollY };
|
|
908
|
+
} catch (err) {
|
|
909
|
+
return { success: false, scrollX: window.scrollX, scrollY: window.scrollY, error: String(err) };
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** @param {unknown} payload */
|
|
914
|
+
async function handleScrollWindow(payload) {
|
|
915
|
+
const p = asPayload(payload);
|
|
916
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
917
|
+
/** Prefer scripting.executeScript so scroll runs in the tab's main frame (not extension/offscreen contexts). */
|
|
918
|
+
const results = await chrome.scripting.executeScript({
|
|
919
|
+
target: { tabId, allFrames: false },
|
|
920
|
+
world: "MAIN",
|
|
921
|
+
injectImmediately: true,
|
|
922
|
+
func: pokeInjectedScrollWindow,
|
|
923
|
+
args: [p],
|
|
924
|
+
});
|
|
925
|
+
const res = /** @type {unknown} */ (results[0]?.result);
|
|
926
|
+
if (res === undefined) {
|
|
927
|
+
throw new Error("scroll_window: no result from executeScript (tab may be restricted or unavailable)");
|
|
928
|
+
}
|
|
929
|
+
return withTabMeta(tabId, res);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** @param {unknown} payload */
|
|
933
|
+
async function handleScreenshot(payload) {
|
|
934
|
+
const p = asPayload(payload);
|
|
935
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
936
|
+
const tab = await ensureTabVisibleForCapture(tabId);
|
|
937
|
+
const fmt = p.format === "jpeg" ? "jpeg" : "png";
|
|
938
|
+
const rawQ = typeof p.quality === "number" ? p.quality : 85;
|
|
939
|
+
/** @type {{ format: 'png' | 'jpeg', quality?: number }} */
|
|
940
|
+
const opts =
|
|
941
|
+
fmt === "jpeg"
|
|
942
|
+
? { format: "jpeg", quality: Math.min(100, Math.max(0, rawQ)) }
|
|
943
|
+
: { format: "png" };
|
|
944
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, opts);
|
|
945
|
+
const m = /^data:([^;]+);base64,(.+)$/.exec(dataUrl);
|
|
946
|
+
if (!m) throw new Error("Invalid screenshot data from browser");
|
|
947
|
+
return withTabMeta(tabId, {
|
|
948
|
+
type: "screenshot_result",
|
|
949
|
+
data: m[2],
|
|
950
|
+
mimeType: m[1],
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** @param {unknown} payload */
|
|
955
|
+
async function handleErrorReporter(payload) {
|
|
956
|
+
const p = asPayload(payload);
|
|
957
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
958
|
+
const limit = typeof p.limit === "number" ? p.limit : 50;
|
|
959
|
+
const res = await chrome.tabs
|
|
960
|
+
.sendMessage(tabId, { type: "POKE_GET_PAGE_ERRORS", limit })
|
|
961
|
+
.catch((e) => {
|
|
962
|
+
throw new Error(`error_reporter relay failed: ${String(e)}`);
|
|
963
|
+
});
|
|
964
|
+
return withTabMeta(tabId, res);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** @param {unknown} payload */
|
|
968
|
+
async function handleGetPerformanceMetrics(payload) {
|
|
969
|
+
const p = asPayload(payload);
|
|
970
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
971
|
+
await debuggerAttachForTool(tabId);
|
|
972
|
+
try {
|
|
973
|
+
const rawMetrics = await debuggerSendWithResult(tabId, "Performance.getMetrics", {});
|
|
974
|
+
const metricsArr = Array.isArray(rawMetrics)
|
|
975
|
+
? rawMetrics
|
|
976
|
+
: rawMetrics && typeof rawMetrics === "object" && "metrics" in rawMetrics
|
|
977
|
+
? /** @type {{ metrics?: unknown }} */ (rawMetrics).metrics
|
|
978
|
+
: null;
|
|
979
|
+
/**
|
|
980
|
+
* @param {string} name
|
|
981
|
+
*/
|
|
982
|
+
const by = (name) => {
|
|
983
|
+
if (!Array.isArray(metricsArr)) return undefined;
|
|
984
|
+
const row = metricsArr.find(
|
|
985
|
+
(x) => x && typeof x === "object" && /** @type {{ name?: string }} */ (x).name === name,
|
|
986
|
+
);
|
|
987
|
+
return row && typeof /** @type {{ value?: number }} */ (row).value === "number"
|
|
988
|
+
? /** @type {{ value: number }} */ (row).value
|
|
989
|
+
: undefined;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const navExpr = `(() => {
|
|
993
|
+
const t = performance.timing;
|
|
994
|
+
const ns = t.navigationStart || 0;
|
|
995
|
+
if (!ns) return { domContentLoaded: null, loadEventEnd: null };
|
|
996
|
+
return {
|
|
997
|
+
domContentLoaded: t.domContentLoadedEventEnd > 0 ? t.domContentLoadedEventEnd - ns : null,
|
|
998
|
+
loadEventEnd: t.loadEventEnd > 0 ? t.loadEventEnd - ns : null,
|
|
999
|
+
};
|
|
1000
|
+
})()`;
|
|
1001
|
+
const navRes = await debuggerSendWithResult(tabId, "Runtime.evaluate", {
|
|
1002
|
+
expression: navExpr,
|
|
1003
|
+
returnByValue: true,
|
|
1004
|
+
});
|
|
1005
|
+
const navVal =
|
|
1006
|
+
navRes && typeof navRes === "object" && "result" in navRes
|
|
1007
|
+
? /** @type {{ result?: { value?: unknown } }} */ (navRes).result?.value
|
|
1008
|
+
: undefined;
|
|
1009
|
+
|
|
1010
|
+
const paintExpr = `(() => {
|
|
1011
|
+
const entries = performance.getEntriesByType("paint");
|
|
1012
|
+
let firstPaint = null;
|
|
1013
|
+
let firstContentfulPaint = null;
|
|
1014
|
+
for (const e of entries) {
|
|
1015
|
+
if (e.name === "first-paint") firstPaint = e.startTime;
|
|
1016
|
+
if (e.name === "first-contentful-paint") firstContentfulPaint = e.startTime;
|
|
1017
|
+
}
|
|
1018
|
+
return { firstPaint, firstContentfulPaint };
|
|
1019
|
+
})()`;
|
|
1020
|
+
const paintRes = await debuggerSendWithResult(tabId, "Runtime.evaluate", {
|
|
1021
|
+
expression: paintExpr,
|
|
1022
|
+
returnByValue: true,
|
|
1023
|
+
});
|
|
1024
|
+
const paintVal =
|
|
1025
|
+
paintRes && typeof paintRes === "object" && "result" in paintRes
|
|
1026
|
+
? /** @type {{ result?: { value?: unknown } }} */ (paintRes).result?.value
|
|
1027
|
+
: undefined;
|
|
1028
|
+
|
|
1029
|
+
const nv = navVal && typeof navVal === "object" ? /** @type {Record<string, unknown>} */ (navVal) : {};
|
|
1030
|
+
const pv = paintVal && typeof paintVal === "object" ? /** @type {Record<string, unknown>} */ (paintVal) : {};
|
|
1031
|
+
|
|
1032
|
+
return withTabMeta(tabId, {
|
|
1033
|
+
domContentLoaded: nv.domContentLoaded ?? null,
|
|
1034
|
+
loadEventEnd: nv.loadEventEnd ?? null,
|
|
1035
|
+
firstPaint: pv.firstPaint ?? null,
|
|
1036
|
+
firstContentfulPaint: pv.firstContentfulPaint ?? null,
|
|
1037
|
+
jsHeapUsed: by("JSHeapUsedSize") ?? null,
|
|
1038
|
+
jsHeapTotal: by("JSHeapTotalSize") ?? null,
|
|
1039
|
+
});
|
|
1040
|
+
} finally {
|
|
1041
|
+
await debuggerDetachForTool(tabId);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* @param {ArrayBuffer} buffer
|
|
1047
|
+
*/
|
|
1048
|
+
function arrayBufferToBase64(buffer) {
|
|
1049
|
+
let binary = "";
|
|
1050
|
+
const bytes = new Uint8Array(buffer);
|
|
1051
|
+
const chunk = 0x8000;
|
|
1052
|
+
for (let i = 0; i < bytes.byteLength; i += chunk) {
|
|
1053
|
+
binary += String.fromCharCode.apply(null, /** @type {number[]} */ (Array.from(bytes.subarray(i, i + chunk))));
|
|
1054
|
+
}
|
|
1055
|
+
return btoa(binary);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* @param {string[]} dataUrls
|
|
1060
|
+
*/
|
|
1061
|
+
async function stitchFullPageScreenshots(dataUrls) {
|
|
1062
|
+
if (dataUrls.length === 0) throw new Error("full_page_capture: no strips");
|
|
1063
|
+
/** @type {ImageBitmap[]} */
|
|
1064
|
+
const bitmaps = [];
|
|
1065
|
+
try {
|
|
1066
|
+
for (const u of dataUrls) {
|
|
1067
|
+
const res = await fetch(u);
|
|
1068
|
+
const blob = await res.blob();
|
|
1069
|
+
const bm = await createImageBitmap(blob);
|
|
1070
|
+
bitmaps.push(bm);
|
|
1071
|
+
}
|
|
1072
|
+
let width = 0;
|
|
1073
|
+
let height = 0;
|
|
1074
|
+
for (const bm of bitmaps) {
|
|
1075
|
+
width = Math.max(width, bm.width);
|
|
1076
|
+
height += bm.height;
|
|
1077
|
+
}
|
|
1078
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
1079
|
+
const ctx = canvas.getContext("2d");
|
|
1080
|
+
if (!ctx) throw new Error("full_page_capture: no 2d context");
|
|
1081
|
+
let y = 0;
|
|
1082
|
+
for (const bm of bitmaps) {
|
|
1083
|
+
ctx.drawImage(bm, 0, y);
|
|
1084
|
+
y += bm.height;
|
|
1085
|
+
}
|
|
1086
|
+
const mimeType = String(dataUrls[0]).startsWith("data:image/jpeg") ? "image/jpeg" : "image/png";
|
|
1087
|
+
const blob = await canvas.convertToBlob({ type: mimeType });
|
|
1088
|
+
const buf = await blob.arrayBuffer();
|
|
1089
|
+
const b64 = arrayBufferToBase64(buf);
|
|
1090
|
+
return `data:${mimeType};base64,${b64}`;
|
|
1091
|
+
} finally {
|
|
1092
|
+
for (const bm of bitmaps) {
|
|
1093
|
+
try {
|
|
1094
|
+
bm.close();
|
|
1095
|
+
} catch {
|
|
1096
|
+
/* ignore */
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** @param {unknown} payload */
|
|
1103
|
+
async function handleFullPageCapture(payload) {
|
|
1104
|
+
const p = asPayload(payload);
|
|
1105
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1106
|
+
const tab = await ensureTabVisibleForCapture(tabId);
|
|
1107
|
+
const fmt = p.format === "jpeg" ? "jpeg" : "png";
|
|
1108
|
+
const rawQ = typeof p.quality === "number" ? p.quality : 85;
|
|
1109
|
+
/** @type {{ format: 'png' | 'jpeg', quality?: number }} */
|
|
1110
|
+
const opts =
|
|
1111
|
+
fmt === "jpeg"
|
|
1112
|
+
? { format: "jpeg", quality: Math.min(100, Math.max(0, rawQ)) }
|
|
1113
|
+
: { format: "png" };
|
|
1114
|
+
|
|
1115
|
+
const info = await chrome.tabs.sendMessage(tabId, { type: "POKE_GET_SCROLL_INFO" }).catch(() => null);
|
|
1116
|
+
if (!info || typeof info !== "object" || typeof /** @type {{ scrollHeight?: unknown }} */ (info).scrollHeight !== "number") {
|
|
1117
|
+
throw new Error("full_page_capture: content script unavailable or invalid scroll info");
|
|
1118
|
+
}
|
|
1119
|
+
const scrollHeight = /** @type {{ scrollHeight: number; innerHeight?: number }} */ (info).scrollHeight;
|
|
1120
|
+
const vh = Math.max(1, Math.floor(/** @type {{ innerHeight?: number }} */ (info).innerHeight || 600));
|
|
1121
|
+
|
|
1122
|
+
/** @type {string[]} */
|
|
1123
|
+
const dataUrls = [];
|
|
1124
|
+
await chrome.tabs.sendMessage(tabId, { type: "POKE_SCROLL_TO", y: 0 });
|
|
1125
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1126
|
+
|
|
1127
|
+
let y = 0;
|
|
1128
|
+
for (;;) {
|
|
1129
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, opts);
|
|
1130
|
+
dataUrls.push(dataUrl);
|
|
1131
|
+
if (y + vh >= scrollHeight - 2) break;
|
|
1132
|
+
y = Math.min(y + vh, Math.max(0, scrollHeight - vh));
|
|
1133
|
+
await chrome.tabs.sendMessage(tabId, { type: "POKE_SCROLL_TO", y });
|
|
1134
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
await chrome.tabs.sendMessage(tabId, { type: "POKE_SCROLL_TO", y: 0 });
|
|
1138
|
+
|
|
1139
|
+
const stitched = await stitchFullPageScreenshots(dataUrls);
|
|
1140
|
+
const m = /^data:([^;]+);base64,(.+)$/.exec(stitched);
|
|
1141
|
+
if (!m) throw new Error("full_page_capture: invalid stitched data URL");
|
|
1142
|
+
return withTabMeta(tabId, {
|
|
1143
|
+
type: "screenshot_result",
|
|
1144
|
+
data: m[2],
|
|
1145
|
+
mimeType: m[1],
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** @param {unknown} payload */
|
|
1150
|
+
async function handlePdfExport(payload) {
|
|
1151
|
+
const p = asPayload(payload);
|
|
1152
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1153
|
+
await ensureTabVisibleForCapture(tabId);
|
|
1154
|
+
await debuggerAttachForTool(tabId);
|
|
1155
|
+
try {
|
|
1156
|
+
const scale = typeof p.scale === "number" && p.scale > 0 ? p.scale : 1;
|
|
1157
|
+
const res = await debuggerSendWithResult(tabId, "Page.printToPDF", {
|
|
1158
|
+
printBackground: true,
|
|
1159
|
+
landscape: p.landscape === true,
|
|
1160
|
+
scale,
|
|
1161
|
+
});
|
|
1162
|
+
const data =
|
|
1163
|
+
res && typeof res === "object" && res !== null && "data" in res
|
|
1164
|
+
? String(/** @type {{ data?: string }} */ (res).data ?? "")
|
|
1165
|
+
: "";
|
|
1166
|
+
if (!data) throw new Error("pdf_export: printToPDF returned no data");
|
|
1167
|
+
return withTabMeta(tabId, { success: true, data, mimeType: "application/pdf" });
|
|
1168
|
+
} finally {
|
|
1169
|
+
await debuggerDetachForTool(tabId);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const DEVICE_PRESETS = {
|
|
1174
|
+
mobile: { width: 390, height: 844, deviceScaleFactor: 3, mobile: true },
|
|
1175
|
+
tablet: { width: 834, height: 1112, deviceScaleFactor: 2, mobile: true },
|
|
1176
|
+
desktop: { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
/** @param {unknown} payload */
|
|
1180
|
+
async function handleDeviceEmulate(payload) {
|
|
1181
|
+
const p = asPayload(payload);
|
|
1182
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1183
|
+
const d = p.device === "mobile" || p.device === "tablet" || p.device === "desktop" ? p.device : "desktop";
|
|
1184
|
+
const preset = DEVICE_PRESETS[d];
|
|
1185
|
+
const width = typeof p.width === "number" ? p.width : preset.width;
|
|
1186
|
+
const height = typeof p.height === "number" ? p.height : preset.height;
|
|
1187
|
+
const deviceScaleFactor =
|
|
1188
|
+
typeof p.deviceScaleFactor === "number" ? p.deviceScaleFactor : preset.deviceScaleFactor;
|
|
1189
|
+
|
|
1190
|
+
await debuggerAttachForTool(tabId);
|
|
1191
|
+
try {
|
|
1192
|
+
await debuggerSend(tabId, "Emulation.setDeviceMetricsOverride", {
|
|
1193
|
+
width: Math.round(width),
|
|
1194
|
+
height: Math.round(height),
|
|
1195
|
+
deviceScaleFactor,
|
|
1196
|
+
mobile: preset.mobile,
|
|
1197
|
+
fitWindow: false,
|
|
1198
|
+
scale: 1,
|
|
1199
|
+
});
|
|
1200
|
+
const ua = typeof p.userAgent === "string" && p.userAgent.trim() ? p.userAgent.trim() : undefined;
|
|
1201
|
+
if (ua) {
|
|
1202
|
+
await debuggerSend(tabId, "Network.setUserAgentOverride", { userAgent: ua });
|
|
1203
|
+
}
|
|
1204
|
+
return withTabMeta(tabId, { success: true });
|
|
1205
|
+
} finally {
|
|
1206
|
+
await debuggerDetachForTool(tabId);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** @param {unknown} payload */
|
|
1211
|
+
async function handleEvaluateJs(payload) {
|
|
1212
|
+
const p = asPayload(payload);
|
|
1213
|
+
const code = typeof p.code === "string" ? p.code : "";
|
|
1214
|
+
if (!code) throw new Error("evaluate_js requires code");
|
|
1215
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1216
|
+
const requestId =
|
|
1217
|
+
typeof p.requestId === "string" ? p.requestId : `bg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1218
|
+
const timeoutMs = typeof p.timeoutMs === "number" ? p.timeoutMs : 30000;
|
|
1219
|
+
const res = await chrome.tabs.sendMessage(tabId, {
|
|
1220
|
+
type: "POKE_EVAL",
|
|
1221
|
+
code,
|
|
1222
|
+
requestId,
|
|
1223
|
+
timeoutMs,
|
|
1224
|
+
}).catch((e) => {
|
|
1225
|
+
throw new Error(`evaluate_js relay failed: ${String(e)}`);
|
|
1226
|
+
});
|
|
1227
|
+
return withTabMeta(tabId, res);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* @param {number} tabId
|
|
1232
|
+
* @param {string} pokeType
|
|
1233
|
+
* @param {Record<string, unknown>} data
|
|
1234
|
+
*/
|
|
1235
|
+
async function sendPerceptionToTab(tabId, pokeType, data) {
|
|
1236
|
+
const res = await chrome.tabs.sendMessage(tabId, { ...data, type: pokeType }).catch((e) => {
|
|
1237
|
+
throw new Error(`Perception relay failed (${pokeType}): ${String(e)}`);
|
|
1238
|
+
});
|
|
1239
|
+
if (res && typeof res === "object" && "error" in res && typeof res.error === "string") {
|
|
1240
|
+
throw new Error(res.error);
|
|
1241
|
+
}
|
|
1242
|
+
return res;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** @param {unknown} payload */
|
|
1246
|
+
async function handleGetDomSnapshot(payload) {
|
|
1247
|
+
const p = asPayload(payload);
|
|
1248
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1249
|
+
const res = await sendPerceptionToTab(tabId, "POKE_GET_DOM_SNAPSHOT", {
|
|
1250
|
+
includeHidden: p.includeHidden === true,
|
|
1251
|
+
maxDepth: typeof p.maxDepth === "number" ? p.maxDepth : undefined,
|
|
1252
|
+
});
|
|
1253
|
+
return withTabMeta(tabId, res);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/** @param {unknown} payload */
|
|
1257
|
+
async function handleGetAccessibilityTree(payload) {
|
|
1258
|
+
const p = asPayload(payload);
|
|
1259
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1260
|
+
const res = await sendPerceptionToTab(tabId, "POKE_GET_A11Y_TREE", {
|
|
1261
|
+
interactiveOnly: p.interactiveOnly === true,
|
|
1262
|
+
});
|
|
1263
|
+
return withTabMeta(tabId, res);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** @param {unknown} payload */
|
|
1267
|
+
async function handleFindElement(payload) {
|
|
1268
|
+
const p = asPayload(payload);
|
|
1269
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1270
|
+
const query = typeof p.query === "string" ? p.query : "";
|
|
1271
|
+
const strategy =
|
|
1272
|
+
p.strategy === "css" || p.strategy === "text" || p.strategy === "aria" || p.strategy === "xpath"
|
|
1273
|
+
? p.strategy
|
|
1274
|
+
: "auto";
|
|
1275
|
+
const res = await sendPerceptionToTab(tabId, "POKE_FIND_ELEMENT", { query, strategy });
|
|
1276
|
+
return withTabMeta(tabId, res);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** @param {unknown} payload */
|
|
1280
|
+
async function handleReadPage(payload) {
|
|
1281
|
+
const p = asPayload(payload);
|
|
1282
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1283
|
+
const format =
|
|
1284
|
+
p.format === "markdown" || p.format === "text" || p.format === "structured" ? p.format : "structured";
|
|
1285
|
+
const res = await sendPerceptionToTab(tabId, "POKE_READ_PAGE", { format });
|
|
1286
|
+
return withTabMeta(tabId, res);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/** @param {unknown} payload */
|
|
1290
|
+
async function handleWaitForSelector(payload) {
|
|
1291
|
+
const p = asPayload(payload);
|
|
1292
|
+
const selector = typeof p.selector === "string" ? p.selector : "";
|
|
1293
|
+
if (!selector.trim()) throw new Error("wait_for_selector requires selector");
|
|
1294
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1295
|
+
const timeout = typeof p.timeout === "number" && p.timeout > 0 ? p.timeout : 10000;
|
|
1296
|
+
const visible = p.visible === true;
|
|
1297
|
+
const res = await chrome.tabs
|
|
1298
|
+
.sendMessage(tabId, { type: "POKE_WAIT_FOR_SELECTOR", selector, timeout, visible })
|
|
1299
|
+
.catch((e) => {
|
|
1300
|
+
throw new Error(`wait_for_selector relay failed: ${String(e)}`);
|
|
1301
|
+
});
|
|
1302
|
+
return withTabMeta(tabId, res);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** @param {unknown} payload */
|
|
1306
|
+
async function handleExecuteScript(payload) {
|
|
1307
|
+
const p = asPayload(payload);
|
|
1308
|
+
const script = typeof p.script === "string" ? p.script : "";
|
|
1309
|
+
if (!script.trim()) throw new Error("execute_script requires script");
|
|
1310
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1311
|
+
const args = Array.isArray(p.args) ? p.args : [];
|
|
1312
|
+
|
|
1313
|
+
const results = await chrome.scripting.executeScript({
|
|
1314
|
+
target: { tabId, allFrames: false },
|
|
1315
|
+
world: "MAIN",
|
|
1316
|
+
injectImmediately: true,
|
|
1317
|
+
func: async (scriptSource, callArgs) => {
|
|
1318
|
+
const seen = new WeakSet();
|
|
1319
|
+
/**
|
|
1320
|
+
* @param {string} _k
|
|
1321
|
+
* @param {unknown} val
|
|
1322
|
+
*/
|
|
1323
|
+
function replacer(_k, val) {
|
|
1324
|
+
if (typeof val === "bigint") return val.toString();
|
|
1325
|
+
if (typeof val === "object" && val !== null) {
|
|
1326
|
+
if (seen.has(/** @type {object} */ (val))) return "[Circular]";
|
|
1327
|
+
seen.add(/** @type {object} */ (val));
|
|
1328
|
+
}
|
|
1329
|
+
return val;
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
1333
|
+
const fn = new AsyncFunction("args", `return (async () => {\n${scriptSource}\n})();`);
|
|
1334
|
+
const raw = await fn(callArgs ?? []);
|
|
1335
|
+
try {
|
|
1336
|
+
return { result: JSON.parse(JSON.stringify(raw, replacer)) };
|
|
1337
|
+
} catch (serErr) {
|
|
1338
|
+
return {
|
|
1339
|
+
result: String(raw),
|
|
1340
|
+
error: `serialization: ${serErr instanceof Error ? serErr.message : String(serErr)}`,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
} catch (e) {
|
|
1344
|
+
return { error: String(e) };
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
args: [script, args],
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
const fr = /** @type {{ result?: unknown; error?: string } | undefined} */ (results[0]?.result);
|
|
1351
|
+
if (!fr) return withTabMeta(tabId, { result: null, error: "No frame result" });
|
|
1352
|
+
if (typeof fr.error === "string" && fr.error && fr.result === undefined) {
|
|
1353
|
+
return withTabMeta(tabId, { result: undefined, error: fr.error });
|
|
1354
|
+
}
|
|
1355
|
+
return withTabMeta(tabId, {
|
|
1356
|
+
result: fr.result,
|
|
1357
|
+
error: typeof fr.error === "string" ? fr.error : undefined,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/** @param {unknown} payload */
|
|
1362
|
+
async function handleGetConsoleLogs(payload) {
|
|
1363
|
+
const p = asPayload(payload);
|
|
1364
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1365
|
+
const level =
|
|
1366
|
+
p.level === "error" || p.level === "warn" || p.level === "info" || p.level === "log" || p.level === "all"
|
|
1367
|
+
? p.level
|
|
1368
|
+
: "all";
|
|
1369
|
+
const limit = typeof p.limit === "number" ? Math.min(500, Math.max(1, p.limit)) : 100;
|
|
1370
|
+
const res = await chrome.tabs
|
|
1371
|
+
.sendMessage(tabId, { type: "POKE_GET_CONSOLE_LOGS", level, limit })
|
|
1372
|
+
.catch((e) => {
|
|
1373
|
+
throw new Error(`get_console_logs relay failed: ${String(e)}`);
|
|
1374
|
+
});
|
|
1375
|
+
const logs = res && typeof res === "object" && "logs" in res ? /** @type {{ logs: unknown }} */ (res).logs : [];
|
|
1376
|
+
const count = res && typeof res === "object" && "count" in res ? Number(/** @type {{ count?: number }} */ (res).count) : 0;
|
|
1377
|
+
return { logs, count, tabId };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/** @param {unknown} payload */
|
|
1381
|
+
async function handleClearConsoleLogs(payload) {
|
|
1382
|
+
const p = asPayload(payload);
|
|
1383
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1384
|
+
await chrome.tabs.sendMessage(tabId, { type: "POKE_CLEAR_CONSOLE_LOGS" }).catch((e) => {
|
|
1385
|
+
throw new Error(`clear_console_logs relay failed: ${String(e)}`);
|
|
1386
|
+
});
|
|
1387
|
+
return { cleared: true, tabId };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/** @param {unknown} payload */
|
|
1391
|
+
async function handleStartNetworkCapture(payload) {
|
|
1392
|
+
const p = asPayload(payload);
|
|
1393
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1394
|
+
networkStateByTab.delete(tabId);
|
|
1395
|
+
if (!networkCaptureTabs.has(tabId)) {
|
|
1396
|
+
await debuggerAttach(tabId);
|
|
1397
|
+
networkCaptureTabs.add(tabId);
|
|
1398
|
+
}
|
|
1399
|
+
await debuggerSend(tabId, "Network.enable", {});
|
|
1400
|
+
return { success: true, tabId, capturing: true };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** @param {unknown} payload */
|
|
1404
|
+
async function handleStopNetworkCapture(payload) {
|
|
1405
|
+
const p = asPayload(payload);
|
|
1406
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1407
|
+
if (!networkCaptureTabs.has(tabId)) {
|
|
1408
|
+
return { success: true, tabId, capturing: false };
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
await debuggerSend(tabId, "Network.disable", {});
|
|
1412
|
+
} catch {
|
|
1413
|
+
/* ignore */
|
|
1414
|
+
}
|
|
1415
|
+
networkCaptureTabs.delete(tabId);
|
|
1416
|
+
await debuggerDetach(tabId);
|
|
1417
|
+
return { success: true, tabId, capturing: false };
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/** @param {unknown} payload */
|
|
1421
|
+
async function handleGetNetworkLogs(payload) {
|
|
1422
|
+
const p = asPayload(payload);
|
|
1423
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1424
|
+
const filter = typeof p.filter === "string" ? p.filter : "";
|
|
1425
|
+
const limit = typeof p.limit === "number" ? Math.min(200, Math.max(1, p.limit)) : 50;
|
|
1426
|
+
const includeBody = p.includeBody === true;
|
|
1427
|
+
|
|
1428
|
+
const state = networkStateByTab.get(tabId);
|
|
1429
|
+
if (!state) {
|
|
1430
|
+
return { requests: [], count: 0 };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** @type {Record<string, unknown>[]} */
|
|
1434
|
+
const rows = [];
|
|
1435
|
+
for (const rid of state.order) {
|
|
1436
|
+
const row = state.byId.get(rid);
|
|
1437
|
+
if (row) rows.push(row);
|
|
1438
|
+
}
|
|
1439
|
+
let filtered = filter ? rows.filter((r) => String(r.url ?? "").includes(filter)) : [...rows];
|
|
1440
|
+
filtered = filtered.slice(-limit);
|
|
1441
|
+
|
|
1442
|
+
const needTempAttach = includeBody && !networkCaptureTabs.has(tabId);
|
|
1443
|
+
if (needTempAttach) {
|
|
1444
|
+
await debuggerAttach(tabId);
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
/** @type {Record<string, unknown>[]} */
|
|
1448
|
+
const out = [];
|
|
1449
|
+
for (const e of filtered) {
|
|
1450
|
+
const copy = { ...e };
|
|
1451
|
+
if (includeBody && e.loaded === true && typeof e.requestId === "string") {
|
|
1452
|
+
try {
|
|
1453
|
+
const bodyRes = /** @type {{ body?: string; base64Encoded?: boolean }} */ (
|
|
1454
|
+
await debuggerSendWithResult(tabId, "Network.getResponseBody", { requestId: e.requestId })
|
|
1455
|
+
);
|
|
1456
|
+
copy.body = bodyRes.body;
|
|
1457
|
+
copy.bodyBase64Encoded = bodyRes.base64Encoded === true;
|
|
1458
|
+
} catch {
|
|
1459
|
+
copy.bodyFetchError = "Network.getResponseBody failed";
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
out.push(copy);
|
|
1463
|
+
}
|
|
1464
|
+
return { requests: out, count: out.length };
|
|
1465
|
+
} finally {
|
|
1466
|
+
if (needTempAttach) {
|
|
1467
|
+
await debuggerDetach(tabId);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** @param {unknown} payload */
|
|
1473
|
+
async function handleClearNetworkLogs(payload) {
|
|
1474
|
+
const p = asPayload(payload);
|
|
1475
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1476
|
+
networkStateByTab.delete(tabId);
|
|
1477
|
+
return { cleared: true, tabId };
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const PERSISTENT_LOADER_ID = "poke-browser-persistent-loader";
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* @param {chrome.cookies.Cookie} c
|
|
1484
|
+
*/
|
|
1485
|
+
function cookieRemoveUrl(c) {
|
|
1486
|
+
const dom = c.domain.startsWith(".") ? c.domain.slice(1) : c.domain;
|
|
1487
|
+
const scheme = c.secure ? "https" : "http";
|
|
1488
|
+
const path = c.path && c.path.length ? c.path : "/";
|
|
1489
|
+
return `${scheme}://${dom}${path}`;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* @param {chrome.cookies.Cookie} c
|
|
1494
|
+
*/
|
|
1495
|
+
function serializeCookie(c) {
|
|
1496
|
+
return {
|
|
1497
|
+
name: c.name,
|
|
1498
|
+
value: c.value,
|
|
1499
|
+
domain: c.domain,
|
|
1500
|
+
path: c.path,
|
|
1501
|
+
secure: c.secure,
|
|
1502
|
+
httpOnly: c.httpOnly,
|
|
1503
|
+
sameSite: c.sameSite,
|
|
1504
|
+
expirationDate: c.expirationDate,
|
|
1505
|
+
session: c.session,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function ensurePersistentLoaderRegistered() {
|
|
1510
|
+
const existing = await chrome.scripting.getRegisteredContentScripts({ ids: [PERSISTENT_LOADER_ID] });
|
|
1511
|
+
if (Array.isArray(existing) && existing.length > 0) return;
|
|
1512
|
+
await chrome.scripting.registerContentScripts([
|
|
1513
|
+
{
|
|
1514
|
+
id: PERSISTENT_LOADER_ID,
|
|
1515
|
+
matches: ["<all_urls>"],
|
|
1516
|
+
js: ["persistent-loader.js"],
|
|
1517
|
+
runAt: "document_start",
|
|
1518
|
+
},
|
|
1519
|
+
]);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* @param {number} tabId
|
|
1524
|
+
*/
|
|
1525
|
+
async function tabHttpUrl(tabId) {
|
|
1526
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1527
|
+
const u = tab.url ?? "";
|
|
1528
|
+
if (!u.startsWith("http://") && !u.startsWith("https://")) {
|
|
1529
|
+
throw new Error("Tab must have an http(s) URL for this operation");
|
|
1530
|
+
}
|
|
1531
|
+
return u;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/** @param {unknown} payload */
|
|
1535
|
+
async function handleScriptInject(payload) {
|
|
1536
|
+
const p = asPayload(payload);
|
|
1537
|
+
const script = typeof p.script === "string" ? p.script : "";
|
|
1538
|
+
if (!script.trim()) throw new Error("script_inject requires script");
|
|
1539
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1540
|
+
await tabHttpUrl(tabId);
|
|
1541
|
+
const persistent = p.persistent === true;
|
|
1542
|
+
const runAt =
|
|
1543
|
+
p.runAt === "document_start" || p.runAt === "document_end" || p.runAt === "document_idle"
|
|
1544
|
+
? p.runAt
|
|
1545
|
+
: "document_idle";
|
|
1546
|
+
|
|
1547
|
+
if (persistent) {
|
|
1548
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1549
|
+
const url = tab.url ?? "";
|
|
1550
|
+
const u = new URL(url);
|
|
1551
|
+
const matchPattern = `${u.origin}/*`;
|
|
1552
|
+
const injectionId = `poke-${crypto.randomUUID()}`;
|
|
1553
|
+
const got = await chrome.storage.local.get("pokePersistentInjections");
|
|
1554
|
+
const list = Array.isArray(got.pokePersistentInjections) ? got.pokePersistentInjections : [];
|
|
1555
|
+
list.push({ id: injectionId, matchPattern, script, runAt });
|
|
1556
|
+
await chrome.storage.local.set({ pokePersistentInjections: list });
|
|
1557
|
+
await ensurePersistentLoaderRegistered();
|
|
1558
|
+
return withTabMeta(tabId, { success: true, injectionId });
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (runAt === "document_idle") {
|
|
1562
|
+
const res = await chrome.tabs.sendMessage(tabId, { type: "POKE_SCRIPT_INJECT", script }).catch((e) => {
|
|
1563
|
+
throw new Error(`script_inject relay failed: ${String(e)}`);
|
|
1564
|
+
});
|
|
1565
|
+
return withTabMeta(tabId, { success: Boolean(res && res.success === true) });
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
await chrome.scripting.executeScript({
|
|
1569
|
+
target: { tabId, allFrames: false },
|
|
1570
|
+
world: "MAIN",
|
|
1571
|
+
injectImmediately: runAt === "document_start",
|
|
1572
|
+
func: (code) => {
|
|
1573
|
+
const s = document.createElement("script");
|
|
1574
|
+
s.textContent = code;
|
|
1575
|
+
const r = document.documentElement || document.head || document.body;
|
|
1576
|
+
if (r) {
|
|
1577
|
+
r.appendChild(s);
|
|
1578
|
+
s.remove();
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
args: [script],
|
|
1582
|
+
});
|
|
1583
|
+
return withTabMeta(tabId, { success: true });
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/** @param {unknown} payload */
|
|
1587
|
+
async function handleCookieManager(payload) {
|
|
1588
|
+
const p = asPayload(payload);
|
|
1589
|
+
const action =
|
|
1590
|
+
p.action === "get" || p.action === "get_all" || p.action === "set" || p.action === "delete" || p.action === "delete_all"
|
|
1591
|
+
? p.action
|
|
1592
|
+
: null;
|
|
1593
|
+
if (!action) throw new Error("cookie_manager requires action");
|
|
1594
|
+
|
|
1595
|
+
const tabId =
|
|
1596
|
+
typeof p.tabId === "number" && Number.isFinite(p.tabId) ? await resolveTabId(p.tabId) : undefined;
|
|
1597
|
+
|
|
1598
|
+
/** @type {string | undefined} */
|
|
1599
|
+
let baseUrl = typeof p.url === "string" && p.url.length > 0 ? p.url : undefined;
|
|
1600
|
+
if (!baseUrl && tabId != null) {
|
|
1601
|
+
try {
|
|
1602
|
+
baseUrl = await tabHttpUrl(tabId);
|
|
1603
|
+
} catch {
|
|
1604
|
+
/* tab may be invalid for http(s); leave baseUrl unset */
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (action === "get") {
|
|
1609
|
+
const name = typeof p.name === "string" ? p.name : "";
|
|
1610
|
+
if (!name) throw new Error("cookie get requires name");
|
|
1611
|
+
if (!baseUrl) throw new Error("cookie get requires url or http(s) tabId");
|
|
1612
|
+
const c = await chrome.cookies.get({ url: baseUrl, name });
|
|
1613
|
+
return { success: true, cookie: c ? serializeCookie(c) : undefined };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (action === "get_all") {
|
|
1617
|
+
/** @type {chrome.cookies.GetAllDetails} */
|
|
1618
|
+
const q = {};
|
|
1619
|
+
if (baseUrl) q.url = baseUrl;
|
|
1620
|
+
const dom = typeof p.domain === "string" && p.domain.length > 0 ? p.domain : undefined;
|
|
1621
|
+
if (dom) q.domain = dom;
|
|
1622
|
+
if (!q.url && !q.domain) throw new Error("cookie get_all requires url/domain or http(s) tabId");
|
|
1623
|
+
const all = await chrome.cookies.getAll(q);
|
|
1624
|
+
const cookies = all.map(serializeCookie);
|
|
1625
|
+
return { success: true, cookie: cookies };
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (action === "set") {
|
|
1629
|
+
const name = typeof p.name === "string" ? p.name : "";
|
|
1630
|
+
if (!name) throw new Error("cookie set requires name");
|
|
1631
|
+
const value = typeof p.value === "string" ? p.value : "";
|
|
1632
|
+
if (!baseUrl && typeof p.domain !== "string") {
|
|
1633
|
+
throw new Error("cookie set requires url or tab with http(s) URL, or domain");
|
|
1634
|
+
}
|
|
1635
|
+
/** @type {chrome.cookies.SetDetails} */
|
|
1636
|
+
const details = { name, value };
|
|
1637
|
+
if (baseUrl) details.url = baseUrl;
|
|
1638
|
+
if (typeof p.domain === "string") details.domain = p.domain;
|
|
1639
|
+
if (typeof p.path === "string") details.path = p.path;
|
|
1640
|
+
if (p.secure === true) details.secure = true;
|
|
1641
|
+
if (p.httpOnly === true) details.httpOnly = true;
|
|
1642
|
+
if (typeof p.expirationDate === "number") details.expirationDate = p.expirationDate;
|
|
1643
|
+
const c = await chrome.cookies.set(details);
|
|
1644
|
+
if (!c) return { success: false, cookie: undefined };
|
|
1645
|
+
return { success: true, cookie: serializeCookie(c) };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (action === "delete") {
|
|
1649
|
+
const name = typeof p.name === "string" ? p.name : "";
|
|
1650
|
+
if (!name) throw new Error("cookie delete requires name");
|
|
1651
|
+
if (!baseUrl) throw new Error("cookie delete requires url or http(s) tabId");
|
|
1652
|
+
const res = await chrome.cookies.remove({ url: baseUrl, name });
|
|
1653
|
+
return { success: Boolean(res) };
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (action === "delete_all") {
|
|
1657
|
+
const dom = typeof p.domain === "string" ? p.domain.trim() : "";
|
|
1658
|
+
if (!dom) throw new Error("cookie delete_all requires domain");
|
|
1659
|
+
const normalized = dom.startsWith(".") ? dom : `.${dom}`;
|
|
1660
|
+
const all = await chrome.cookies.getAll({ domain: normalized });
|
|
1661
|
+
for (const c of all) {
|
|
1662
|
+
const u = cookieRemoveUrl(c);
|
|
1663
|
+
await chrome.cookies.remove({ url: u, name: c.name });
|
|
1664
|
+
}
|
|
1665
|
+
return { success: true, cookie: all.map(serializeCookie) };
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
throw new Error("cookie_manager: unsupported action");
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/** @param {unknown} payload */
|
|
1672
|
+
async function handleFillForm(payload) {
|
|
1673
|
+
const p = asPayload(payload);
|
|
1674
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1675
|
+
const fields = Array.isArray(p.fields) ? p.fields : [];
|
|
1676
|
+
const res = await chrome.tabs
|
|
1677
|
+
.sendMessage(tabId, {
|
|
1678
|
+
type: "POKE_FILL_FORM",
|
|
1679
|
+
fields,
|
|
1680
|
+
submitAfter: p.submitAfter === true,
|
|
1681
|
+
submitSelector: typeof p.submitSelector === "string" ? p.submitSelector : undefined,
|
|
1682
|
+
})
|
|
1683
|
+
.catch((e) => {
|
|
1684
|
+
throw new Error(`fill_form relay failed: ${String(e)}`);
|
|
1685
|
+
});
|
|
1686
|
+
return withTabMeta(tabId, res);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/** @param {unknown} payload */
|
|
1690
|
+
async function handleGetStorage(payload) {
|
|
1691
|
+
const p = asPayload(payload);
|
|
1692
|
+
const type = p.type === "local" || p.type === "session" || p.type === "cookie" ? p.type : "local";
|
|
1693
|
+
const key = typeof p.key === "string" ? p.key : undefined;
|
|
1694
|
+
|
|
1695
|
+
if (type === "cookie") {
|
|
1696
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1697
|
+
const url = await tabHttpUrl(tabId);
|
|
1698
|
+
const all = await chrome.cookies.getAll({ url });
|
|
1699
|
+
/** @type {Record<string, string>} */
|
|
1700
|
+
const data = {};
|
|
1701
|
+
for (const c of all) {
|
|
1702
|
+
if (key && c.name !== key) continue;
|
|
1703
|
+
data[c.name] = c.value;
|
|
1704
|
+
}
|
|
1705
|
+
return { data, count: Object.keys(data).length };
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1709
|
+
const res = await chrome.tabs
|
|
1710
|
+
.sendMessage(tabId, {
|
|
1711
|
+
type: "POKE_GET_STORAGE",
|
|
1712
|
+
storageType: type,
|
|
1713
|
+
key,
|
|
1714
|
+
})
|
|
1715
|
+
.catch((e) => {
|
|
1716
|
+
throw new Error(`get_storage relay failed: ${String(e)}`);
|
|
1717
|
+
});
|
|
1718
|
+
return res;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/** @param {unknown} payload */
|
|
1722
|
+
async function handleSetStorage(payload) {
|
|
1723
|
+
const p = asPayload(payload);
|
|
1724
|
+
const type = p.type === "local" || p.type === "session" ? p.type : "local";
|
|
1725
|
+
const key = typeof p.key === "string" ? p.key : "";
|
|
1726
|
+
const value = typeof p.value === "string" ? p.value : "";
|
|
1727
|
+
if (!key) throw new Error("set_storage requires key");
|
|
1728
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1729
|
+
const res = await chrome.tabs
|
|
1730
|
+
.sendMessage(tabId, {
|
|
1731
|
+
type: "POKE_SET_STORAGE",
|
|
1732
|
+
storageType: type,
|
|
1733
|
+
key,
|
|
1734
|
+
value,
|
|
1735
|
+
})
|
|
1736
|
+
.catch((e) => {
|
|
1737
|
+
throw new Error(`set_storage relay failed: ${String(e)}`);
|
|
1738
|
+
});
|
|
1739
|
+
return res;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/** @param {unknown} payload */
|
|
1743
|
+
async function handleHoverElement(payload) {
|
|
1744
|
+
const p = asPayload(payload);
|
|
1745
|
+
const tabId = await resolveTabId(typeof p.tabId === "number" ? p.tabId : undefined);
|
|
1746
|
+
const selector = typeof p.selector === "string" ? p.selector.trim() : "";
|
|
1747
|
+
const x = typeof p.x === "number" ? p.x : Number(p.x);
|
|
1748
|
+
const y = typeof p.y === "number" ? p.y : Number(p.y);
|
|
1749
|
+
const hasXY = Number.isFinite(x) && Number.isFinite(y);
|
|
1750
|
+
|
|
1751
|
+
if (selector) {
|
|
1752
|
+
const res = await chrome.tabs.sendMessage(tabId, { type: "POKE_HOVER_ELEMENT", selector }).catch((e) => {
|
|
1753
|
+
throw new Error(`hover_element relay failed: ${String(e)}`);
|
|
1754
|
+
});
|
|
1755
|
+
return withTabMeta(tabId, res);
|
|
1756
|
+
}
|
|
1757
|
+
if (hasXY) {
|
|
1758
|
+
await showCursorFeedbackDot(tabId, x, y);
|
|
1759
|
+
await debuggerAttachForTool(tabId);
|
|
1760
|
+
try {
|
|
1761
|
+
await debuggerSend(tabId, "Input.dispatchMouseEvent", {
|
|
1762
|
+
type: "mouseMoved",
|
|
1763
|
+
x,
|
|
1764
|
+
y,
|
|
1765
|
+
});
|
|
1766
|
+
return withTabMeta(tabId, { success: true });
|
|
1767
|
+
} finally {
|
|
1768
|
+
await debuggerDetachForTool(tabId);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
throw new Error("hover_element requires selector or numeric x and y");
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/** @param {unknown} payload */
|
|
1775
|
+
async function handleNewTab(payload) {
|
|
1776
|
+
const p = asPayload(payload);
|
|
1777
|
+
const url = typeof p.url === "string" && p.url.length > 0 ? p.url : "about:blank";
|
|
1778
|
+
const tab = await chrome.tabs.create({ url, active: p.active !== false });
|
|
1779
|
+
if (tab.id == null) throw new Error("Failed to create tab");
|
|
1780
|
+
return { tabId: tab.id };
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/** @param {unknown} payload */
|
|
1784
|
+
async function handleCloseTab(payload) {
|
|
1785
|
+
const p = asPayload(payload);
|
|
1786
|
+
if (typeof p.tabId !== "number" || !Number.isFinite(p.tabId)) {
|
|
1787
|
+
throw new Error("close_tab requires tabId");
|
|
1788
|
+
}
|
|
1789
|
+
await chrome.tabs.get(p.tabId).catch(() => {
|
|
1790
|
+
throw new Error(`Tab not found: ${p.tabId}`);
|
|
1791
|
+
});
|
|
1792
|
+
await chrome.tabs.remove(p.tabId);
|
|
1793
|
+
return { closed: true, tabId: p.tabId };
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/** @param {unknown} payload */
|
|
1797
|
+
async function handleSwitchTab(payload) {
|
|
1798
|
+
const p = asPayload(payload);
|
|
1799
|
+
if (typeof p.tabId !== "number" || !Number.isFinite(p.tabId)) {
|
|
1800
|
+
throw new Error("switch_tab requires tabId");
|
|
1801
|
+
}
|
|
1802
|
+
const tab = await chrome.tabs.get(p.tabId).catch(() => null);
|
|
1803
|
+
if (!tab?.id) throw new Error(`Tab not found: ${p.tabId}`);
|
|
1804
|
+
await chrome.tabs.update(p.tabId, { active: true });
|
|
1805
|
+
if (tab.windowId != null) {
|
|
1806
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
1807
|
+
}
|
|
1808
|
+
return { tabId: p.tabId, active: true };
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** @type {Record<string, (payload: unknown) => Promise<unknown>>} */
|
|
1812
|
+
const COMMAND_HANDLERS = {
|
|
1813
|
+
list_tabs: handleListTabs,
|
|
1814
|
+
get_active_tab: handleGetActiveTab,
|
|
1815
|
+
navigate_to: handleNavigateTo,
|
|
1816
|
+
click_element: handleClickElement,
|
|
1817
|
+
type_text: handleTypeText,
|
|
1818
|
+
scroll_window: handleScrollWindow,
|
|
1819
|
+
screenshot: handleScreenshot,
|
|
1820
|
+
evaluate_js: handleEvaluateJs,
|
|
1821
|
+
new_tab: handleNewTab,
|
|
1822
|
+
close_tab: handleCloseTab,
|
|
1823
|
+
switch_tab: handleSwitchTab,
|
|
1824
|
+
get_dom_snapshot: handleGetDomSnapshot,
|
|
1825
|
+
get_accessibility_tree: handleGetAccessibilityTree,
|
|
1826
|
+
find_element: handleFindElement,
|
|
1827
|
+
read_page: handleReadPage,
|
|
1828
|
+
wait_for_selector: handleWaitForSelector,
|
|
1829
|
+
execute_script: handleExecuteScript,
|
|
1830
|
+
get_console_logs: handleGetConsoleLogs,
|
|
1831
|
+
clear_console_logs: handleClearConsoleLogs,
|
|
1832
|
+
get_network_logs: handleGetNetworkLogs,
|
|
1833
|
+
clear_network_logs: handleClearNetworkLogs,
|
|
1834
|
+
start_network_capture: handleStartNetworkCapture,
|
|
1835
|
+
stop_network_capture: handleStopNetworkCapture,
|
|
1836
|
+
hover_element: handleHoverElement,
|
|
1837
|
+
script_inject: handleScriptInject,
|
|
1838
|
+
cookie_manager: handleCookieManager,
|
|
1839
|
+
fill_form: handleFillForm,
|
|
1840
|
+
get_storage: handleGetStorage,
|
|
1841
|
+
set_storage: handleSetStorage,
|
|
1842
|
+
error_reporter: handleErrorReporter,
|
|
1843
|
+
get_performance_metrics: handleGetPerformanceMetrics,
|
|
1844
|
+
full_page_capture: handleFullPageCapture,
|
|
1845
|
+
pdf_export: handlePdfExport,
|
|
1846
|
+
device_emulate: handleDeviceEmulate,
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* @param {string} command
|
|
1851
|
+
* @param {unknown} payload
|
|
1852
|
+
*/
|
|
1853
|
+
async function dispatchCommand(command, payload) {
|
|
1854
|
+
const handler = COMMAND_HANDLERS[command];
|
|
1855
|
+
if (!handler) throw new Error(`Unknown command: ${command}`);
|
|
1856
|
+
return handler(payload);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1860
|
+
chrome.storage.local.get(["wsPort", "enabled"]).then((v) => {
|
|
1861
|
+
const patch = {};
|
|
1862
|
+
if (v.wsPort == null) patch.wsPort = DEFAULT_WS_PORT;
|
|
1863
|
+
if (v.enabled === undefined) patch.enabled = true;
|
|
1864
|
+
if (Object.keys(patch).length) chrome.storage.local.set(patch);
|
|
1865
|
+
});
|
|
1866
|
+
void (async () => {
|
|
1867
|
+
if (await getPokeEnabled()) {
|
|
1868
|
+
await ensureOffscreenAndSchedule();
|
|
1869
|
+
} else {
|
|
1870
|
+
scheduleKeepAliveAlarm();
|
|
1871
|
+
}
|
|
1872
|
+
})();
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
1876
|
+
void (async () => {
|
|
1877
|
+
if (await getPokeEnabled()) {
|
|
1878
|
+
await ensureOffscreenAndSchedule();
|
|
1879
|
+
} else {
|
|
1880
|
+
scheduleKeepAliveAlarm();
|
|
1881
|
+
}
|
|
1882
|
+
})();
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
void (async () => {
|
|
1886
|
+
if (await getPokeEnabled()) {
|
|
1887
|
+
await ensureOffscreenAndSchedule();
|
|
1888
|
+
} else {
|
|
1889
|
+
scheduleKeepAliveAlarm();
|
|
1890
|
+
}
|
|
1891
|
+
})();
|
|
1892
|
+
|
|
1893
|
+
/** @type {Record<string, (message: unknown, sendResponse: (r: unknown) => void) => boolean | void>} */
|
|
1894
|
+
const RUNTIME_HANDLERS = {
|
|
1895
|
+
POKE_GET_STATE: (message, sendResponse) => {
|
|
1896
|
+
void Promise.all([getWsPort(), chrome.storage.local.get("wsAuthToken")]).then(([port, st]) => {
|
|
1897
|
+
const tok = st && typeof st.wsAuthToken === "string" ? st.wsAuthToken : "";
|
|
1898
|
+
sendResponse({
|
|
1899
|
+
status: mcpStatus,
|
|
1900
|
+
port,
|
|
1901
|
+
log: commandLog,
|
|
1902
|
+
hasAuthToken: tok.length > 0,
|
|
1903
|
+
});
|
|
1904
|
+
});
|
|
1905
|
+
return true;
|
|
1906
|
+
},
|
|
1907
|
+
POKE_SET_TOKEN: (message, sendResponse) => {
|
|
1908
|
+
const m = /** @type {{ token?: unknown }} */ (message);
|
|
1909
|
+
const token = typeof m.token === "string" ? m.token : "";
|
|
1910
|
+
void chrome.storage.local.set({ wsAuthToken: token }).then(async () => {
|
|
1911
|
+
if (await getPokeEnabled()) {
|
|
1912
|
+
await ensureOffscreenAndSchedule();
|
|
1913
|
+
try {
|
|
1914
|
+
bridgePort?.postMessage({ type: "reconnect" });
|
|
1915
|
+
} catch {
|
|
1916
|
+
/* ignore */
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
sendResponse({ ok: true });
|
|
1920
|
+
});
|
|
1921
|
+
return true;
|
|
1922
|
+
},
|
|
1923
|
+
POKE_SET_PORT: (message, sendResponse) => {
|
|
1924
|
+
const m = /** @type {{ port?: unknown }} */ (message);
|
|
1925
|
+
const next = Number(m.port);
|
|
1926
|
+
if (!Number.isFinite(next) || next <= 0 || next >= 65536) {
|
|
1927
|
+
sendResponse({ ok: false, error: "Invalid port" });
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
void chrome.storage.local.set({ wsPort: next }).then(async () => {
|
|
1931
|
+
if (await getPokeEnabled()) {
|
|
1932
|
+
await ensureOffscreenAndSchedule();
|
|
1933
|
+
try {
|
|
1934
|
+
bridgePort?.postMessage({ type: "reconnect", port: next });
|
|
1935
|
+
} catch {
|
|
1936
|
+
/* ignore */
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
sendResponse({ ok: true, port: next });
|
|
1940
|
+
});
|
|
1941
|
+
return true;
|
|
1942
|
+
},
|
|
1943
|
+
POKE_RECONNECT: (_message, sendResponse) => {
|
|
1944
|
+
void (async () => {
|
|
1945
|
+
if (await getPokeEnabled()) {
|
|
1946
|
+
await ensureOffscreenAndSchedule();
|
|
1947
|
+
try {
|
|
1948
|
+
bridgePort?.postMessage({ type: "reconnect" });
|
|
1949
|
+
} catch {
|
|
1950
|
+
/* ignore */
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
sendResponse({ ok: true });
|
|
1954
|
+
})();
|
|
1955
|
+
return true;
|
|
1956
|
+
},
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1959
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1960
|
+
if (message && typeof message === "object" && message.action === "reconnect") {
|
|
1961
|
+
const wsUrl =
|
|
1962
|
+
typeof message.wsUrl === "string" && message.wsUrl.trim() ? message.wsUrl.trim() : "";
|
|
1963
|
+
void (async () => {
|
|
1964
|
+
if (wsUrl) {
|
|
1965
|
+
await chrome.storage.local.set({ wsUrl });
|
|
1966
|
+
}
|
|
1967
|
+
if (!(await getPokeEnabled())) {
|
|
1968
|
+
sendResponse({ ok: true });
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
await ensureOffscreenAndSchedule();
|
|
1972
|
+
try {
|
|
1973
|
+
bridgePort?.postMessage(wsUrl ? { type: "reconnect", wsUrl } : { type: "reconnect" });
|
|
1974
|
+
} catch {
|
|
1975
|
+
/* ignore */
|
|
1976
|
+
}
|
|
1977
|
+
sendResponse({ ok: true });
|
|
1978
|
+
})();
|
|
1979
|
+
return true;
|
|
1980
|
+
}
|
|
1981
|
+
if (message && typeof message === "object" && message.action === "setPokeBrowserEnabled") {
|
|
1982
|
+
const enabled = message.enabled === true;
|
|
1983
|
+
void (async () => {
|
|
1984
|
+
await chrome.storage.local.set({ enabled });
|
|
1985
|
+
if (enabled) {
|
|
1986
|
+
await ensureOffscreenAndSchedule();
|
|
1987
|
+
} else {
|
|
1988
|
+
await stopMcpConnection();
|
|
1989
|
+
}
|
|
1990
|
+
sendResponse({ ok: true });
|
|
1991
|
+
})();
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
const t = message && typeof message === "object" && "type" in message ? String(message.type) : "";
|
|
1995
|
+
const fn = RUNTIME_HANDLERS[t];
|
|
1996
|
+
if (fn) return fn(message, sendResponse);
|
|
1997
|
+
return undefined;
|
|
1998
|
+
});
|