@flrande/bak-extension 0.3.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +1305 -140
- package/dist/content.global.js +820 -3
- package/dist/manifest.json +2 -2
- package/package.json +2 -2
- package/public/manifest.json +2 -2
- package/src/background.ts +1762 -992
- package/src/content.ts +2419 -1593
- package/src/network-debugger.ts +495 -0
- package/src/privacy.ts +112 -1
- package/src/session-binding-storage.ts +68 -0
- package/src/workspace.ts +912 -917
package/src/background.ts
CHANGED
|
@@ -1,616 +1,1096 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ConsoleEntry,
|
|
3
|
+
DebugDumpSection,
|
|
4
|
+
Locator,
|
|
5
|
+
NetworkEntry,
|
|
6
|
+
PageExecutionScope,
|
|
7
|
+
PageFetchResponse,
|
|
8
|
+
PageFrameResult,
|
|
9
|
+
PageFreshnessResult
|
|
10
|
+
} from '@flrande/bak-protocol';
|
|
11
|
+
import {
|
|
12
|
+
clearNetworkEntries,
|
|
13
|
+
dropNetworkCapture,
|
|
14
|
+
ensureNetworkDebugger,
|
|
15
|
+
exportHar,
|
|
16
|
+
getNetworkEntry,
|
|
17
|
+
latestNetworkTimestamp,
|
|
18
|
+
listNetworkEntries,
|
|
19
|
+
recentNetworkSampleIds,
|
|
20
|
+
searchNetworkEntries,
|
|
21
|
+
waitForNetworkEntry
|
|
22
|
+
} from './network-debugger.js';
|
|
2
23
|
import { isSupportedAutomationUrl } from './url-policy.js';
|
|
3
24
|
import { computeReconnectDelayMs } from './reconnect.js';
|
|
4
25
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface
|
|
21
|
-
id: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
LEGACY_STORAGE_KEY_WORKSPACE,
|
|
27
|
+
LEGACY_STORAGE_KEY_WORKSPACES,
|
|
28
|
+
resolveSessionBindingStateMap,
|
|
29
|
+
STORAGE_KEY_SESSION_BINDINGS
|
|
30
|
+
} from './session-binding-storage.js';
|
|
31
|
+
import { containsRedactionMarker } from './privacy.js';
|
|
32
|
+
import {
|
|
33
|
+
type WorkspaceBrowser as SessionBindingBrowser,
|
|
34
|
+
type WorkspaceColor as SessionBindingColor,
|
|
35
|
+
type WorkspaceRecord as SessionBindingRecord,
|
|
36
|
+
type WorkspaceTab as SessionBindingTab,
|
|
37
|
+
type WorkspaceWindow as SessionBindingWindow,
|
|
38
|
+
WorkspaceManager as SessionBindingManager
|
|
39
|
+
} from './workspace.js';
|
|
40
|
+
|
|
41
|
+
interface CliRequest {
|
|
42
|
+
id: string;
|
|
43
|
+
method: string;
|
|
44
|
+
params?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface CliResponse {
|
|
48
|
+
id: string;
|
|
49
|
+
ok: boolean;
|
|
50
|
+
result?: unknown;
|
|
51
|
+
error?: {
|
|
52
|
+
code: string;
|
|
53
|
+
message: string;
|
|
54
|
+
data?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ExtensionConfig {
|
|
59
|
+
token: string;
|
|
60
|
+
port: number;
|
|
61
|
+
debugRichText: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface RuntimeErrorDetails {
|
|
65
|
+
message: string;
|
|
66
|
+
context: 'config' | 'socket' | 'request' | 'parse';
|
|
67
|
+
at: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const DEFAULT_PORT = 17373;
|
|
71
|
+
const STORAGE_KEY_TOKEN = 'pairToken';
|
|
45
72
|
const STORAGE_KEY_PORT = 'cliPort';
|
|
46
73
|
const STORAGE_KEY_DEBUG_RICH_TEXT = 'debugRichText';
|
|
47
|
-
const STORAGE_KEY_WORKSPACE = 'agentWorkspace';
|
|
48
74
|
const DEFAULT_TAB_LOAD_TIMEOUT_MS = 40_000;
|
|
75
|
+
const textEncoder = new TextEncoder();
|
|
76
|
+
const textDecoder = new TextDecoder();
|
|
77
|
+
const REPLAY_FORBIDDEN_HEADER_NAMES = new Set([
|
|
78
|
+
'accept-encoding',
|
|
79
|
+
'authorization',
|
|
80
|
+
'connection',
|
|
81
|
+
'content-length',
|
|
82
|
+
'cookie',
|
|
83
|
+
'host',
|
|
84
|
+
'origin',
|
|
85
|
+
'proxy-authorization',
|
|
86
|
+
'referer',
|
|
87
|
+
'set-cookie'
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
let ws: WebSocket | null = null;
|
|
91
|
+
let reconnectTimer: number | null = null;
|
|
92
|
+
let nextReconnectInMs: number | null = null;
|
|
93
|
+
let reconnectAttempt = 0;
|
|
94
|
+
let lastError: RuntimeErrorDetails | null = null;
|
|
95
|
+
let manualDisconnect = false;
|
|
96
|
+
|
|
97
|
+
async function getConfig(): Promise<ExtensionConfig> {
|
|
98
|
+
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
99
|
+
return {
|
|
100
|
+
token: typeof stored[STORAGE_KEY_TOKEN] === 'string' ? stored[STORAGE_KEY_TOKEN] : '',
|
|
101
|
+
port: typeof stored[STORAGE_KEY_PORT] === 'number' ? stored[STORAGE_KEY_PORT] : DEFAULT_PORT,
|
|
102
|
+
debugRichText: stored[STORAGE_KEY_DEBUG_RICH_TEXT] === true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function setConfig(config: Partial<ExtensionConfig>): Promise<void> {
|
|
107
|
+
const payload: Record<string, unknown> = {};
|
|
108
|
+
if (typeof config.token === 'string') {
|
|
109
|
+
payload[STORAGE_KEY_TOKEN] = config.token;
|
|
110
|
+
}
|
|
111
|
+
if (typeof config.port === 'number') {
|
|
112
|
+
payload[STORAGE_KEY_PORT] = config.port;
|
|
113
|
+
}
|
|
114
|
+
if (typeof config.debugRichText === 'boolean') {
|
|
115
|
+
payload[STORAGE_KEY_DEBUG_RICH_TEXT] = config.debugRichText;
|
|
116
|
+
}
|
|
117
|
+
if (Object.keys(payload).length > 0) {
|
|
118
|
+
await chrome.storage.local.set(payload);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setRuntimeError(message: string, context: RuntimeErrorDetails['context']): void {
|
|
123
|
+
lastError = {
|
|
124
|
+
message,
|
|
125
|
+
context,
|
|
126
|
+
at: Date.now()
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function clearReconnectTimer(): void {
|
|
131
|
+
if (reconnectTimer !== null) {
|
|
132
|
+
clearTimeout(reconnectTimer);
|
|
133
|
+
reconnectTimer = null;
|
|
134
|
+
}
|
|
135
|
+
nextReconnectInMs = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sendResponse(payload: CliResponse): void {
|
|
139
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
140
|
+
ws.send(JSON.stringify(payload));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function toError(code: string, message: string, data?: Record<string, unknown>): CliResponse['error'] {
|
|
145
|
+
return { code, message, data };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeUnhandledError(error: unknown): CliResponse['error'] {
|
|
149
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
150
|
+
return error as CliResponse['error'];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
const lower = message.toLowerCase();
|
|
155
|
+
|
|
156
|
+
if (lower.includes('no tab with id') || lower.includes('no window with id')) {
|
|
157
|
+
return toError('E_NOT_FOUND', message);
|
|
158
|
+
}
|
|
159
|
+
if (lower.includes('workspace') && lower.includes('does not exist')) {
|
|
160
|
+
return toError('E_NOT_FOUND', message);
|
|
161
|
+
}
|
|
162
|
+
if (lower.includes('does not belong to workspace') || lower.includes('is missing from workspace')) {
|
|
163
|
+
return toError('E_NOT_FOUND', message);
|
|
164
|
+
}
|
|
165
|
+
if (lower.includes('invalid url') || lower.includes('url is invalid')) {
|
|
166
|
+
return toError('E_INVALID_PARAMS', message);
|
|
167
|
+
}
|
|
168
|
+
if (lower.includes('cannot access contents of url') || lower.includes('permission denied')) {
|
|
169
|
+
return toError('E_PERMISSION', message);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return toError('E_INTERNAL', message);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toTabInfo(tab: chrome.tabs.Tab): SessionBindingTab {
|
|
176
|
+
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
177
|
+
throw new Error('Tab is missing runtime identifiers');
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
id: tab.id,
|
|
181
|
+
title: tab.title ?? '',
|
|
182
|
+
url: tab.url ?? '',
|
|
183
|
+
active: Boolean(tab.active),
|
|
184
|
+
windowId: tab.windowId,
|
|
185
|
+
groupId: typeof tab.groupId === 'number' && tab.groupId >= 0 ? tab.groupId : null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function loadWorkspaceStateMap(): Promise<Record<string, SessionBindingRecord>> {
|
|
190
|
+
const stored = await chrome.storage.local.get([
|
|
191
|
+
STORAGE_KEY_SESSION_BINDINGS,
|
|
192
|
+
LEGACY_STORAGE_KEY_WORKSPACES,
|
|
193
|
+
LEGACY_STORAGE_KEY_WORKSPACE
|
|
194
|
+
]);
|
|
195
|
+
return resolveSessionBindingStateMap(stored);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function loadWorkspaceState(workspaceId: string): Promise<SessionBindingRecord | null> {
|
|
199
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
200
|
+
return stateMap[workspaceId] ?? null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function listWorkspaceStates(): Promise<SessionBindingRecord[]> {
|
|
204
|
+
return Object.values(await loadWorkspaceStateMap());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function saveWorkspaceState(state: SessionBindingRecord): Promise<void> {
|
|
208
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
209
|
+
stateMap[state.id] = state;
|
|
210
|
+
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
211
|
+
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function deleteWorkspaceState(workspaceId: string): Promise<void> {
|
|
215
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
216
|
+
delete stateMap[workspaceId];
|
|
217
|
+
if (Object.keys(stateMap).length === 0) {
|
|
218
|
+
await chrome.storage.local.remove([STORAGE_KEY_SESSION_BINDINGS, LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
222
|
+
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const workspaceBrowser: SessionBindingBrowser = {
|
|
226
|
+
async getTab(tabId) {
|
|
227
|
+
try {
|
|
228
|
+
return toTabInfo(await chrome.tabs.get(tabId));
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
async getActiveTab() {
|
|
234
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
235
|
+
const tab = tabs[0];
|
|
236
|
+
if (!tab) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return toTabInfo(tab);
|
|
240
|
+
},
|
|
241
|
+
async listTabs(filter) {
|
|
242
|
+
const tabs = await chrome.tabs.query(filter?.windowId ? { windowId: filter.windowId } : {});
|
|
243
|
+
return tabs
|
|
244
|
+
.filter((tab): tab is chrome.tabs.Tab => typeof tab.id === 'number' && typeof tab.windowId === 'number')
|
|
245
|
+
.map((tab) => toTabInfo(tab));
|
|
246
|
+
},
|
|
247
|
+
async createTab(options) {
|
|
248
|
+
const createdTab = await chrome.tabs.create({
|
|
249
|
+
windowId: options.windowId,
|
|
250
|
+
url: options.url ?? 'about:blank',
|
|
251
|
+
active: options.active
|
|
252
|
+
});
|
|
253
|
+
if (!createdTab) {
|
|
254
|
+
throw new Error('Tab creation returned no tab');
|
|
255
|
+
}
|
|
256
|
+
return toTabInfo(createdTab);
|
|
257
|
+
},
|
|
258
|
+
async updateTab(tabId, options) {
|
|
259
|
+
const updatedTab = await chrome.tabs.update(tabId, {
|
|
260
|
+
active: options.active,
|
|
261
|
+
url: options.url
|
|
262
|
+
});
|
|
263
|
+
if (!updatedTab) {
|
|
264
|
+
throw new Error(`Tab update returned no tab for ${tabId}`);
|
|
265
|
+
}
|
|
266
|
+
return toTabInfo(updatedTab);
|
|
267
|
+
},
|
|
268
|
+
async closeTab(tabId) {
|
|
269
|
+
await chrome.tabs.remove(tabId);
|
|
270
|
+
},
|
|
271
|
+
async getWindow(windowId) {
|
|
272
|
+
try {
|
|
273
|
+
const window = await chrome.windows.get(windowId);
|
|
274
|
+
return {
|
|
275
|
+
id: window.id!,
|
|
276
|
+
focused: Boolean(window.focused)
|
|
277
|
+
} satisfies SessionBindingWindow;
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
async createWindow(options) {
|
|
283
|
+
const previouslyFocusedWindow =
|
|
284
|
+
options.focused === true
|
|
285
|
+
? null
|
|
286
|
+
: (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === 'number') ?? null;
|
|
287
|
+
const previouslyFocusedTab =
|
|
288
|
+
previouslyFocusedWindow?.id !== undefined
|
|
289
|
+
? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === 'number') ?? null
|
|
290
|
+
: null;
|
|
291
|
+
const created = await chrome.windows.create({
|
|
292
|
+
url: options.url ?? 'about:blank',
|
|
293
|
+
focused: true
|
|
294
|
+
});
|
|
295
|
+
if (!created || typeof created.id !== 'number') {
|
|
296
|
+
throw new Error('Window missing id');
|
|
297
|
+
}
|
|
298
|
+
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
299
|
+
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
300
|
+
if (typeof previouslyFocusedTab?.id === 'number') {
|
|
301
|
+
await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const finalWindow = await chrome.windows.get(created.id);
|
|
305
|
+
return {
|
|
306
|
+
id: finalWindow.id!,
|
|
307
|
+
focused: Boolean(finalWindow.focused)
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
async updateWindow(windowId, options) {
|
|
311
|
+
const updated = await chrome.windows.update(windowId, {
|
|
312
|
+
focused: options.focused
|
|
313
|
+
});
|
|
314
|
+
if (!updated || typeof updated.id !== 'number') {
|
|
315
|
+
throw new Error('Window missing id');
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
id: updated.id,
|
|
319
|
+
focused: Boolean(updated.focused)
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
async closeWindow(windowId) {
|
|
323
|
+
await chrome.windows.remove(windowId);
|
|
324
|
+
},
|
|
325
|
+
async getGroup(groupId) {
|
|
326
|
+
try {
|
|
327
|
+
const group = await chrome.tabGroups.get(groupId);
|
|
328
|
+
return {
|
|
329
|
+
id: group.id,
|
|
330
|
+
windowId: group.windowId,
|
|
331
|
+
title: group.title ?? '',
|
|
332
|
+
color: group.color as SessionBindingColor,
|
|
333
|
+
collapsed: Boolean(group.collapsed)
|
|
334
|
+
};
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
async groupTabs(tabIds, groupId) {
|
|
340
|
+
return await chrome.tabs.group({
|
|
341
|
+
tabIds: tabIds as [number, ...number[]],
|
|
342
|
+
groupId
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
async updateGroup(groupId, options) {
|
|
346
|
+
const updated = await chrome.tabGroups.update(groupId, {
|
|
347
|
+
title: options.title,
|
|
348
|
+
color: options.color,
|
|
349
|
+
collapsed: options.collapsed
|
|
350
|
+
});
|
|
351
|
+
if (!updated) {
|
|
352
|
+
throw new Error(`Tab group update returned no group for ${groupId}`);
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
id: updated.id,
|
|
356
|
+
windowId: updated.windowId,
|
|
357
|
+
title: updated.title ?? '',
|
|
358
|
+
color: updated.color as SessionBindingColor,
|
|
359
|
+
collapsed: Boolean(updated.collapsed)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const bindingManager = new SessionBindingManager(
|
|
365
|
+
{
|
|
366
|
+
load: loadWorkspaceState,
|
|
367
|
+
save: saveWorkspaceState,
|
|
368
|
+
delete: deleteWorkspaceState,
|
|
369
|
+
list: listWorkspaceStates
|
|
370
|
+
},
|
|
371
|
+
workspaceBrowser
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
async function waitForTabComplete(tabId: number, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS): Promise<void> {
|
|
375
|
+
try {
|
|
376
|
+
const current = await chrome.tabs.get(tabId);
|
|
377
|
+
if (current.status === 'complete') {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await new Promise<void>((resolve, reject) => {
|
|
385
|
+
let done = false;
|
|
386
|
+
const probeStatus = (): void => {
|
|
387
|
+
void chrome.tabs
|
|
388
|
+
.get(tabId)
|
|
389
|
+
.then((tab) => {
|
|
390
|
+
if (tab.status === 'complete') {
|
|
391
|
+
finish();
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
.catch(() => {
|
|
395
|
+
finish(new Error(`tab removed before load complete: ${tabId}`));
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const finish = (error?: Error): void => {
|
|
400
|
+
if (done) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
done = true;
|
|
404
|
+
clearTimeout(timeoutTimer);
|
|
405
|
+
clearInterval(pollTimer);
|
|
406
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
407
|
+
chrome.tabs.onRemoved.removeListener(onRemoved);
|
|
408
|
+
if (error) {
|
|
409
|
+
reject(error);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
resolve();
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const onUpdated = (updatedTabId: number, changeInfo: { status?: string }): void => {
|
|
416
|
+
if (updatedTabId !== tabId) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (changeInfo.status === 'complete') {
|
|
420
|
+
finish();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const onRemoved = (removedTabId: number): void => {
|
|
425
|
+
if (removedTabId === tabId) {
|
|
426
|
+
finish(new Error(`tab removed before load complete: ${tabId}`));
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const pollTimer = setInterval(probeStatus, 250);
|
|
431
|
+
const timeoutTimer = setTimeout(() => {
|
|
432
|
+
finish(new Error(`tab load timeout: ${tabId}`));
|
|
433
|
+
}, timeoutMs);
|
|
434
|
+
|
|
435
|
+
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
436
|
+
chrome.tabs.onRemoved.addListener(onRemoved);
|
|
437
|
+
probeStatus();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function waitForTabUrl(tabId: number, expectedUrl: string, timeoutMs = 10_000): Promise<void> {
|
|
442
|
+
const normalizedExpectedUrl = normalizeComparableTabUrl(expectedUrl);
|
|
443
|
+
const deadline = Date.now() + timeoutMs;
|
|
444
|
+
while (Date.now() < deadline) {
|
|
445
|
+
try {
|
|
446
|
+
const tab = await chrome.tabs.get(tabId);
|
|
447
|
+
const currentUrl = tab.url ?? '';
|
|
448
|
+
const pendingUrl = 'pendingUrl' in tab && typeof tab.pendingUrl === 'string' ? tab.pendingUrl : '';
|
|
449
|
+
if (
|
|
450
|
+
normalizeComparableTabUrl(currentUrl) === normalizedExpectedUrl ||
|
|
451
|
+
normalizeComparableTabUrl(pendingUrl) === normalizedExpectedUrl
|
|
452
|
+
) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
// Ignore transient lookup failures while the tab is navigating.
|
|
457
|
+
}
|
|
458
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function normalizeComparableTabUrl(url: string): string {
|
|
465
|
+
const raw = url.trim();
|
|
466
|
+
if (!raw) {
|
|
467
|
+
return raw;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const parsed = new URL(raw);
|
|
471
|
+
parsed.hash = '';
|
|
472
|
+
return parsed.href;
|
|
473
|
+
} catch {
|
|
474
|
+
return raw;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function finalizeOpenedWorkspaceTab(
|
|
479
|
+
opened: Awaited<ReturnType<SessionBindingManager['openTab']>>,
|
|
480
|
+
expectedUrl?: string
|
|
481
|
+
): Promise<Awaited<ReturnType<SessionBindingManager['openTab']>>> {
|
|
482
|
+
if (expectedUrl && expectedUrl !== 'about:blank') {
|
|
483
|
+
await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => undefined);
|
|
484
|
+
}
|
|
485
|
+
let refreshedTab = opened.tab;
|
|
486
|
+
try {
|
|
487
|
+
const rawTab = await chrome.tabs.get(opened.tab.id);
|
|
488
|
+
const pendingUrl = 'pendingUrl' in rawTab && typeof rawTab.pendingUrl === 'string' ? rawTab.pendingUrl : '';
|
|
489
|
+
const currentUrl = rawTab.url ?? '';
|
|
490
|
+
const effectiveUrl =
|
|
491
|
+
currentUrl && currentUrl !== 'about:blank'
|
|
492
|
+
? currentUrl
|
|
493
|
+
: pendingUrl && pendingUrl !== 'about:blank'
|
|
494
|
+
? pendingUrl
|
|
495
|
+
: currentUrl || pendingUrl || opened.tab.url;
|
|
496
|
+
refreshedTab = {
|
|
497
|
+
...toTabInfo(rawTab),
|
|
498
|
+
url: effectiveUrl
|
|
499
|
+
};
|
|
500
|
+
} catch {
|
|
501
|
+
refreshedTab = (await workspaceBrowser.getTab(opened.tab.id)) ?? opened.tab;
|
|
502
|
+
}
|
|
503
|
+
const refreshedWorkspace = (await bindingManager.getWorkspaceInfo(opened.workspace.id)) ?? {
|
|
504
|
+
...opened.workspace,
|
|
505
|
+
tabs: opened.workspace.tabs.map((tab) => (tab.id === refreshedTab.id ? refreshedTab : tab))
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
workspace: refreshedWorkspace,
|
|
510
|
+
tab: refreshedTab
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
interface WithTabOptions {
|
|
515
|
+
requireSupportedAutomationUrl?: boolean;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function withTab(target: { tabId?: number; workspaceId?: string } = {}, options: WithTabOptions = {}): Promise<chrome.tabs.Tab> {
|
|
519
|
+
const requireSupportedAutomationUrl = options.requireSupportedAutomationUrl !== false;
|
|
520
|
+
const validate = (tab: chrome.tabs.Tab): chrome.tabs.Tab => {
|
|
521
|
+
if (!tab.id) {
|
|
522
|
+
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
523
|
+
}
|
|
524
|
+
const pendingUrl = 'pendingUrl' in tab && typeof tab.pendingUrl === 'string' ? tab.pendingUrl : '';
|
|
525
|
+
if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab.url) && !isSupportedAutomationUrl(pendingUrl)) {
|
|
526
|
+
throw toError('E_PERMISSION', 'Unsupported tab URL: only http/https pages can be automated', {
|
|
527
|
+
url: tab.url ?? pendingUrl ?? ''
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return tab;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
if (typeof target.tabId === 'number') {
|
|
534
|
+
const tab = await chrome.tabs.get(target.tabId);
|
|
535
|
+
return validate(tab);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const resolved = await bindingManager.resolveTarget({
|
|
539
|
+
tabId: target.tabId,
|
|
540
|
+
workspaceId: typeof target.workspaceId === 'string' ? target.workspaceId : undefined,
|
|
541
|
+
createIfMissing: false
|
|
542
|
+
});
|
|
543
|
+
const tab = await chrome.tabs.get(resolved.tab.id);
|
|
544
|
+
return validate(tab);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function captureAlignedTabScreenshot(tab: chrome.tabs.Tab): Promise<string> {
|
|
548
|
+
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
549
|
+
throw toError('E_NOT_FOUND', 'Tab screenshot requires tab id and window id');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const activeTabs = await chrome.tabs.query({ windowId: tab.windowId, active: true });
|
|
553
|
+
const activeTab = activeTabs[0];
|
|
554
|
+
const shouldSwitch = activeTab?.id !== tab.id;
|
|
555
|
+
|
|
556
|
+
if (shouldSwitch) {
|
|
557
|
+
await chrome.tabs.update(tab.id, { active: true });
|
|
558
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
return await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
|
|
563
|
+
} finally {
|
|
564
|
+
if (shouldSwitch && typeof activeTab?.id === 'number') {
|
|
565
|
+
try {
|
|
566
|
+
await chrome.tabs.update(activeTab.id, { active: true });
|
|
567
|
+
} catch {
|
|
568
|
+
// Ignore restore errors if the original tab no longer exists.
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function sendToContent<TResponse>(tabId: number, message: Record<string, unknown>): Promise<TResponse> {
|
|
575
|
+
const maxAttempts = 10;
|
|
576
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
577
|
+
try {
|
|
578
|
+
const response = await chrome.tabs.sendMessage(tabId, message);
|
|
579
|
+
if (typeof response === 'undefined') {
|
|
580
|
+
throw new Error('Content script returned undefined response');
|
|
581
|
+
}
|
|
582
|
+
return response as TResponse;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
585
|
+
const retriable =
|
|
586
|
+
detail.includes('Receiving end does not exist') ||
|
|
587
|
+
detail.includes('Could not establish connection') ||
|
|
588
|
+
detail.includes('No tab with id') ||
|
|
589
|
+
detail.includes('message port closed before a response was received') ||
|
|
590
|
+
detail.includes('Content script returned undefined response');
|
|
591
|
+
if (!retriable || attempt >= maxAttempts) {
|
|
592
|
+
throw toError('E_NOT_READY', 'Content script unavailable', { detail });
|
|
593
|
+
}
|
|
594
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
throw toError('E_NOT_READY', 'Content script unavailable');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
interface FocusContext {
|
|
602
|
+
windowId: number | null;
|
|
603
|
+
tabId: number | null;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function captureFocusContext(): Promise<FocusContext> {
|
|
607
|
+
const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
608
|
+
const activeTab = activeTabs.find((tab) => typeof tab.id === 'number' && typeof tab.windowId === 'number') ?? null;
|
|
609
|
+
return {
|
|
610
|
+
windowId: activeTab?.windowId ?? null,
|
|
611
|
+
tabId: activeTab?.id ?? null
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function restoreFocusContext(context: FocusContext): Promise<void> {
|
|
616
|
+
if (context.windowId !== null) {
|
|
617
|
+
try {
|
|
618
|
+
await chrome.windows.update(context.windowId, { focused: true });
|
|
619
|
+
} catch {
|
|
620
|
+
// Ignore restore errors if the original window no longer exists.
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (context.tabId !== null) {
|
|
624
|
+
try {
|
|
625
|
+
await chrome.tabs.update(context.tabId, { active: true });
|
|
626
|
+
} catch {
|
|
627
|
+
// Ignore restore errors if the original tab no longer exists.
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
|
|
633
|
+
if (!enabled) {
|
|
634
|
+
return action();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const focusContext = await captureFocusContext();
|
|
638
|
+
try {
|
|
639
|
+
return await action();
|
|
640
|
+
} finally {
|
|
641
|
+
await restoreFocusContext(focusContext);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function requireRpcEnvelope(
|
|
646
|
+
method: string,
|
|
647
|
+
value: unknown
|
|
648
|
+
): { ok: boolean; result?: unknown; error?: CliResponse['error'] } {
|
|
649
|
+
if (typeof value !== 'object' || value === null || typeof (value as { ok?: unknown }).ok !== 'boolean') {
|
|
650
|
+
throw toError('E_NOT_READY', `Content script returned malformed response for ${method}`);
|
|
651
|
+
}
|
|
652
|
+
return value as { ok: boolean; result?: unknown; error?: CliResponse['error'] };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function forwardContentRpc(
|
|
656
|
+
tabId: number,
|
|
657
|
+
method: string,
|
|
658
|
+
params: Record<string, unknown>
|
|
659
|
+
): Promise<unknown> {
|
|
660
|
+
const raw = await sendToContent<unknown>(tabId, {
|
|
661
|
+
type: 'bak.rpc',
|
|
662
|
+
method,
|
|
663
|
+
params
|
|
664
|
+
});
|
|
665
|
+
const response = requireRpcEnvelope(method, raw);
|
|
666
|
+
|
|
667
|
+
if (!response.ok) {
|
|
668
|
+
throw response.error ?? toError('E_INTERNAL', `${method} failed`);
|
|
669
|
+
}
|
|
49
670
|
|
|
50
|
-
|
|
51
|
-
let reconnectTimer: number | null = null;
|
|
52
|
-
let nextReconnectInMs: number | null = null;
|
|
53
|
-
let reconnectAttempt = 0;
|
|
54
|
-
let lastError: RuntimeErrorDetails | null = null;
|
|
55
|
-
let manualDisconnect = false;
|
|
56
|
-
|
|
57
|
-
async function getConfig(): Promise<ExtensionConfig> {
|
|
58
|
-
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
59
|
-
return {
|
|
60
|
-
token: typeof stored[STORAGE_KEY_TOKEN] === 'string' ? stored[STORAGE_KEY_TOKEN] : '',
|
|
61
|
-
port: typeof stored[STORAGE_KEY_PORT] === 'number' ? stored[STORAGE_KEY_PORT] : DEFAULT_PORT,
|
|
62
|
-
debugRichText: stored[STORAGE_KEY_DEBUG_RICH_TEXT] === true
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function setConfig(config: Partial<ExtensionConfig>): Promise<void> {
|
|
67
|
-
const payload: Record<string, unknown> = {};
|
|
68
|
-
if (typeof config.token === 'string') {
|
|
69
|
-
payload[STORAGE_KEY_TOKEN] = config.token;
|
|
70
|
-
}
|
|
71
|
-
if (typeof config.port === 'number') {
|
|
72
|
-
payload[STORAGE_KEY_PORT] = config.port;
|
|
73
|
-
}
|
|
74
|
-
if (typeof config.debugRichText === 'boolean') {
|
|
75
|
-
payload[STORAGE_KEY_DEBUG_RICH_TEXT] = config.debugRichText;
|
|
76
|
-
}
|
|
77
|
-
if (Object.keys(payload).length > 0) {
|
|
78
|
-
await chrome.storage.local.set(payload);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function setRuntimeError(message: string, context: RuntimeErrorDetails['context']): void {
|
|
83
|
-
lastError = {
|
|
84
|
-
message,
|
|
85
|
-
context,
|
|
86
|
-
at: Date.now()
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function clearReconnectTimer(): void {
|
|
91
|
-
if (reconnectTimer !== null) {
|
|
92
|
-
clearTimeout(reconnectTimer);
|
|
93
|
-
reconnectTimer = null;
|
|
94
|
-
}
|
|
95
|
-
nextReconnectInMs = null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function sendResponse(payload: CliResponse): void {
|
|
99
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
100
|
-
ws.send(JSON.stringify(payload));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function toError(code: string, message: string, data?: Record<string, unknown>): CliResponse['error'] {
|
|
105
|
-
return { code, message, data };
|
|
671
|
+
return response.result;
|
|
106
672
|
}
|
|
107
673
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (lower.includes('no tab with id') || lower.includes('no window with id')) {
|
|
117
|
-
return toError('E_NOT_FOUND', message);
|
|
118
|
-
}
|
|
119
|
-
if (lower.includes('invalid url') || lower.includes('url is invalid')) {
|
|
120
|
-
return toError('E_INVALID_PARAMS', message);
|
|
121
|
-
}
|
|
122
|
-
if (lower.includes('cannot access contents of url') || lower.includes('permission denied')) {
|
|
123
|
-
return toError('E_PERMISSION', message);
|
|
674
|
+
async function ensureTabNetworkCapture(tabId: number): Promise<void> {
|
|
675
|
+
try {
|
|
676
|
+
await ensureNetworkDebugger(tabId);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
throw toError('E_DEBUGGER_NOT_ATTACHED', 'Debugger-backed network capture unavailable', {
|
|
679
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
680
|
+
});
|
|
124
681
|
}
|
|
125
|
-
|
|
126
|
-
return toError('E_INTERNAL', message);
|
|
127
682
|
}
|
|
128
683
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
throw new Error('Tab is missing runtime identifiers');
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
id: tab.id,
|
|
135
|
-
title: tab.title ?? '',
|
|
136
|
-
url: tab.url ?? '',
|
|
137
|
-
active: Boolean(tab.active),
|
|
138
|
-
windowId: tab.windowId,
|
|
139
|
-
groupId: typeof tab.groupId === 'number' && tab.groupId >= 0 ? tab.groupId : null
|
|
140
|
-
};
|
|
684
|
+
function normalizePageExecutionScope(value: unknown): PageExecutionScope {
|
|
685
|
+
return value === 'main' || value === 'all-frames' ? value : 'current';
|
|
141
686
|
}
|
|
142
687
|
|
|
143
|
-
async function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
688
|
+
async function currentContextFramePath(tabId: number): Promise<string[]> {
|
|
689
|
+
try {
|
|
690
|
+
const context = (await forwardContentRpc(tabId, 'context.get', { tabId })) as { framePath?: string[] };
|
|
691
|
+
return Array.isArray(context.framePath) ? context.framePath.map(String) : [];
|
|
692
|
+
} catch {
|
|
693
|
+
return [];
|
|
148
694
|
}
|
|
149
|
-
return state as WorkspaceRecord;
|
|
150
695
|
}
|
|
151
696
|
|
|
152
|
-
async function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
697
|
+
async function executePageWorld<T>(
|
|
698
|
+
tabId: number,
|
|
699
|
+
action: 'eval' | 'extract' | 'fetch',
|
|
700
|
+
params: Record<string, unknown>
|
|
701
|
+
): Promise<{ scope: PageExecutionScope; result?: PageFrameResult<T>; results?: Array<PageFrameResult<T>> }> {
|
|
702
|
+
const scope = normalizePageExecutionScope(params.scope);
|
|
703
|
+
const framePath = scope === 'current' ? await currentContextFramePath(tabId) : [];
|
|
704
|
+
const target: chrome.scripting.InjectionTarget =
|
|
705
|
+
scope === 'all-frames'
|
|
706
|
+
? { tabId, allFrames: true }
|
|
707
|
+
: {
|
|
708
|
+
tabId,
|
|
709
|
+
frameIds: [0]
|
|
710
|
+
};
|
|
159
711
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return tabs
|
|
179
|
-
.filter((tab): tab is chrome.tabs.Tab => typeof tab.id === 'number' && typeof tab.windowId === 'number')
|
|
180
|
-
.map((tab) => toTabInfo(tab));
|
|
181
|
-
},
|
|
182
|
-
async createTab(options) {
|
|
183
|
-
const createdTab = await chrome.tabs.create({
|
|
184
|
-
windowId: options.windowId,
|
|
185
|
-
url: options.url ?? 'about:blank',
|
|
186
|
-
active: options.active
|
|
187
|
-
});
|
|
188
|
-
if (!createdTab) {
|
|
189
|
-
throw new Error('Tab creation returned no tab');
|
|
190
|
-
}
|
|
191
|
-
return toTabInfo(createdTab);
|
|
192
|
-
},
|
|
193
|
-
async updateTab(tabId, options) {
|
|
194
|
-
const updatedTab = await chrome.tabs.update(tabId, {
|
|
195
|
-
active: options.active,
|
|
196
|
-
url: options.url
|
|
197
|
-
});
|
|
198
|
-
if (!updatedTab) {
|
|
199
|
-
throw new Error(`Tab update returned no tab for ${tabId}`);
|
|
200
|
-
}
|
|
201
|
-
return toTabInfo(updatedTab);
|
|
202
|
-
},
|
|
203
|
-
async closeTab(tabId) {
|
|
204
|
-
await chrome.tabs.remove(tabId);
|
|
205
|
-
},
|
|
206
|
-
async getWindow(windowId) {
|
|
207
|
-
try {
|
|
208
|
-
const window = await chrome.windows.get(windowId);
|
|
209
|
-
return {
|
|
210
|
-
id: window.id!,
|
|
211
|
-
focused: Boolean(window.focused)
|
|
212
|
-
} satisfies WorkspaceWindow;
|
|
213
|
-
} catch {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
async createWindow(options) {
|
|
218
|
-
const previouslyFocusedWindow =
|
|
219
|
-
options.focused === true
|
|
220
|
-
? null
|
|
221
|
-
: (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === 'number') ?? null;
|
|
222
|
-
const previouslyFocusedTab =
|
|
223
|
-
previouslyFocusedWindow?.id !== undefined
|
|
224
|
-
? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === 'number') ?? null
|
|
225
|
-
: null;
|
|
226
|
-
const created = await chrome.windows.create({
|
|
227
|
-
url: options.url ?? 'about:blank',
|
|
228
|
-
focused: true
|
|
229
|
-
});
|
|
230
|
-
if (!created || typeof created.id !== 'number') {
|
|
231
|
-
throw new Error('Window missing id');
|
|
232
|
-
}
|
|
233
|
-
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
234
|
-
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
235
|
-
if (typeof previouslyFocusedTab?.id === 'number') {
|
|
236
|
-
await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
|
|
712
|
+
const results = await chrome.scripting.executeScript({
|
|
713
|
+
target,
|
|
714
|
+
world: 'MAIN',
|
|
715
|
+
args: [
|
|
716
|
+
{
|
|
717
|
+
action,
|
|
718
|
+
scope,
|
|
719
|
+
framePath,
|
|
720
|
+
expr: typeof params.expr === 'string' ? params.expr : '',
|
|
721
|
+
path: typeof params.path === 'string' ? params.path : '',
|
|
722
|
+
url: typeof params.url === 'string' ? params.url : '',
|
|
723
|
+
method: typeof params.method === 'string' ? params.method : 'GET',
|
|
724
|
+
headers: typeof params.headers === 'object' && params.headers !== null ? params.headers : undefined,
|
|
725
|
+
body: typeof params.body === 'string' ? params.body : undefined,
|
|
726
|
+
contentType: typeof params.contentType === 'string' ? params.contentType : undefined,
|
|
727
|
+
mode: params.mode === 'json' ? 'json' : 'raw',
|
|
728
|
+
maxBytes: typeof params.maxBytes === 'number' ? params.maxBytes : undefined,
|
|
729
|
+
timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : undefined
|
|
237
730
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
async getGroup(groupId) {
|
|
261
|
-
try {
|
|
262
|
-
const group = await chrome.tabGroups.get(groupId);
|
|
263
|
-
return {
|
|
264
|
-
id: group.id,
|
|
265
|
-
windowId: group.windowId,
|
|
266
|
-
title: group.title ?? '',
|
|
267
|
-
color: group.color as WorkspaceColor,
|
|
268
|
-
collapsed: Boolean(group.collapsed)
|
|
731
|
+
],
|
|
732
|
+
func: async (payload) => {
|
|
733
|
+
const serializeValue = (value: unknown, maxBytes?: number) => {
|
|
734
|
+
let cloned: unknown;
|
|
735
|
+
try {
|
|
736
|
+
cloned = typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
737
|
+
} catch (error) {
|
|
738
|
+
throw {
|
|
739
|
+
code: 'E_NOT_SERIALIZABLE',
|
|
740
|
+
message: error instanceof Error ? error.message : String(error)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const json = JSON.stringify(cloned);
|
|
744
|
+
const bytes = typeof json === 'string' ? json.length : 0;
|
|
745
|
+
if (typeof maxBytes === 'number' && maxBytes > 0 && bytes > maxBytes) {
|
|
746
|
+
throw {
|
|
747
|
+
code: 'E_BODY_TOO_LARGE',
|
|
748
|
+
message: 'serialized value exceeds max-bytes',
|
|
749
|
+
details: { bytes, maxBytes }
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
return { value: cloned, bytes };
|
|
269
753
|
};
|
|
270
|
-
} catch {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
async groupTabs(tabIds, groupId) {
|
|
275
|
-
return await chrome.tabs.group({
|
|
276
|
-
tabIds: tabIds as [number, ...number[]],
|
|
277
|
-
groupId
|
|
278
|
-
});
|
|
279
|
-
},
|
|
280
|
-
async updateGroup(groupId, options) {
|
|
281
|
-
const updated = await chrome.tabGroups.update(groupId, {
|
|
282
|
-
title: options.title,
|
|
283
|
-
color: options.color,
|
|
284
|
-
collapsed: options.collapsed
|
|
285
|
-
});
|
|
286
|
-
if (!updated) {
|
|
287
|
-
throw new Error(`Tab group update returned no group for ${groupId}`);
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
id: updated.id,
|
|
291
|
-
windowId: updated.windowId,
|
|
292
|
-
title: updated.title ?? '',
|
|
293
|
-
color: updated.color as WorkspaceColor,
|
|
294
|
-
collapsed: Boolean(updated.collapsed)
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const workspaceManager = new WorkspaceManager(
|
|
300
|
-
{
|
|
301
|
-
load: loadWorkspaceState,
|
|
302
|
-
save: saveWorkspaceState
|
|
303
|
-
},
|
|
304
|
-
workspaceBrowser
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
async function waitForTabComplete(tabId: number, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS): Promise<void> {
|
|
308
|
-
try {
|
|
309
|
-
const current = await chrome.tabs.get(tabId);
|
|
310
|
-
if (current.status === 'complete') {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
} catch {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
754
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
755
|
+
const parsePath = (path: string): Array<string | number> => {
|
|
756
|
+
if (typeof path !== 'string' || !path.trim()) {
|
|
757
|
+
throw { code: 'E_INVALID_PARAMS', message: 'path is required' };
|
|
758
|
+
}
|
|
759
|
+
const normalized = path.replace(/^globalThis\.?/, '').replace(/^window\.?/, '').trim();
|
|
760
|
+
if (!normalized) {
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
const segments: Array<string | number> = [];
|
|
764
|
+
let index = 0;
|
|
765
|
+
while (index < normalized.length) {
|
|
766
|
+
if (normalized[index] === '.') {
|
|
767
|
+
index += 1;
|
|
768
|
+
continue;
|
|
325
769
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
resolve();
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
const onUpdated = (updatedTabId: number, changeInfo: { status?: string }): void => {
|
|
349
|
-
if (updatedTabId !== tabId) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (changeInfo.status === 'complete') {
|
|
353
|
-
finish();
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
const onRemoved = (removedTabId: number): void => {
|
|
358
|
-
if (removedTabId === tabId) {
|
|
359
|
-
finish(new Error(`tab removed before load complete: ${tabId}`));
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
const pollTimer = setInterval(probeStatus, 250);
|
|
364
|
-
const timeoutTimer = setTimeout(() => {
|
|
365
|
-
finish(new Error(`tab load timeout: ${tabId}`));
|
|
366
|
-
}, timeoutMs);
|
|
770
|
+
if (normalized[index] === '[') {
|
|
771
|
+
const bracket = normalized.slice(index).match(/^\[(\d+)\]/);
|
|
772
|
+
if (!bracket) {
|
|
773
|
+
throw { code: 'E_INVALID_PARAMS', message: 'Only numeric bracket paths are supported' };
|
|
774
|
+
}
|
|
775
|
+
segments.push(Number(bracket[1]));
|
|
776
|
+
index += bracket[0].length;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const identifier = normalized.slice(index).match(/^[A-Za-z_$][\w$]*/);
|
|
780
|
+
if (!identifier) {
|
|
781
|
+
throw { code: 'E_INVALID_PARAMS', message: `Unsupported path token near: ${normalized.slice(index, index + 16)}` };
|
|
782
|
+
}
|
|
783
|
+
segments.push(identifier[0]);
|
|
784
|
+
index += identifier[0].length;
|
|
785
|
+
}
|
|
786
|
+
return segments;
|
|
787
|
+
};
|
|
367
788
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
789
|
+
const resolveFrameWindow = (frameSelectors: string[]) => {
|
|
790
|
+
let currentWindow: Window = window;
|
|
791
|
+
let currentDocument: Document = document;
|
|
792
|
+
for (const selector of frameSelectors) {
|
|
793
|
+
const frame = currentDocument.querySelector(selector);
|
|
794
|
+
if (!frame || !('contentWindow' in frame)) {
|
|
795
|
+
throw { code: 'E_NOT_FOUND', message: `frame not found: ${selector}` };
|
|
796
|
+
}
|
|
797
|
+
const nextWindow = (frame as HTMLIFrameElement).contentWindow;
|
|
798
|
+
if (!nextWindow) {
|
|
799
|
+
throw { code: 'E_NOT_READY', message: `frame window unavailable: ${selector}` };
|
|
800
|
+
}
|
|
801
|
+
currentWindow = nextWindow;
|
|
802
|
+
currentDocument = nextWindow.document;
|
|
803
|
+
}
|
|
804
|
+
return currentWindow;
|
|
805
|
+
};
|
|
373
806
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
807
|
+
try {
|
|
808
|
+
const targetWindow = payload.scope === 'main' ? window : payload.scope === 'current' ? resolveFrameWindow(payload.framePath ?? []) : window;
|
|
809
|
+
if (payload.action === 'eval') {
|
|
810
|
+
const evaluator = (targetWindow as Window & { eval: (expr: string) => unknown }).eval;
|
|
811
|
+
const serialized = serializeValue(evaluator(payload.expr), payload.maxBytes);
|
|
812
|
+
return { url: targetWindow.location.href, framePath: payload.scope === 'current' ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
|
|
813
|
+
}
|
|
814
|
+
if (payload.action === 'extract') {
|
|
815
|
+
const segments = parsePath(payload.path);
|
|
816
|
+
let current: unknown = targetWindow;
|
|
817
|
+
for (const segment of segments) {
|
|
818
|
+
if (current === null || current === undefined || !(segment in (current as Record<string | number, unknown>))) {
|
|
819
|
+
throw { code: 'E_NOT_FOUND', message: `path not found: ${payload.path}` };
|
|
820
|
+
}
|
|
821
|
+
current = (current as Record<string | number, unknown>)[segment];
|
|
822
|
+
}
|
|
823
|
+
const serialized = serializeValue(current, payload.maxBytes);
|
|
824
|
+
return { url: targetWindow.location.href, framePath: payload.scope === 'current' ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
|
|
825
|
+
}
|
|
826
|
+
if (payload.action === 'fetch') {
|
|
827
|
+
const headers = { ...(payload.headers ?? {}) } as Record<string, string>;
|
|
828
|
+
if (payload.contentType && !headers['Content-Type']) {
|
|
829
|
+
headers['Content-Type'] = payload.contentType;
|
|
830
|
+
}
|
|
831
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
832
|
+
const timeoutId =
|
|
833
|
+
controller && typeof payload.timeoutMs === 'number' && payload.timeoutMs > 0
|
|
834
|
+
? window.setTimeout(() => controller.abort(), payload.timeoutMs)
|
|
835
|
+
: null;
|
|
836
|
+
let response: Response;
|
|
837
|
+
try {
|
|
838
|
+
response = await targetWindow.fetch(payload.url, {
|
|
839
|
+
method: payload.method || 'GET',
|
|
840
|
+
headers,
|
|
841
|
+
body: typeof payload.body === 'string' ? payload.body : undefined,
|
|
842
|
+
signal: controller ? controller.signal : undefined
|
|
843
|
+
});
|
|
844
|
+
} finally {
|
|
845
|
+
if (timeoutId !== null) {
|
|
846
|
+
window.clearTimeout(timeoutId);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const bodyText = await response.text();
|
|
850
|
+
const headerMap: Record<string, string> = {};
|
|
851
|
+
response.headers.forEach((value, key) => {
|
|
852
|
+
headerMap[key] = value;
|
|
853
|
+
});
|
|
854
|
+
return {
|
|
855
|
+
url: targetWindow.location.href,
|
|
856
|
+
framePath: payload.scope === 'current' ? payload.framePath ?? [] : [],
|
|
857
|
+
value: (() => {
|
|
858
|
+
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
|
|
859
|
+
const decoder = typeof TextDecoder === 'function' ? new TextDecoder() : null;
|
|
860
|
+
const previewLimit = typeof payload.maxBytes === 'number' && payload.maxBytes > 0 ? payload.maxBytes : 8192;
|
|
861
|
+
const encodedBody = encoder ? encoder.encode(bodyText) : null;
|
|
862
|
+
const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
|
|
863
|
+
const truncated = bodyBytes > previewLimit;
|
|
864
|
+
if (payload.mode === 'json' && truncated) {
|
|
865
|
+
throw {
|
|
866
|
+
code: 'E_BODY_TOO_LARGE',
|
|
867
|
+
message: 'JSON response exceeds max-bytes',
|
|
868
|
+
details: {
|
|
869
|
+
bytes: bodyBytes,
|
|
870
|
+
maxBytes: previewLimit
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const previewText =
|
|
875
|
+
encodedBody && decoder
|
|
876
|
+
? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit)))
|
|
877
|
+
: truncated
|
|
878
|
+
? bodyText.slice(0, previewLimit)
|
|
879
|
+
: bodyText;
|
|
880
|
+
return {
|
|
881
|
+
url: response.url,
|
|
882
|
+
status: response.status,
|
|
883
|
+
ok: response.ok,
|
|
884
|
+
headers: headerMap,
|
|
885
|
+
contentType: response.headers.get('content-type') ?? undefined,
|
|
886
|
+
bodyText: payload.mode === 'json' ? undefined : previewText,
|
|
887
|
+
json: payload.mode === 'json' && bodyText ? JSON.parse(bodyText) : undefined,
|
|
888
|
+
bytes: bodyBytes,
|
|
889
|
+
truncated
|
|
890
|
+
};
|
|
891
|
+
})()
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
throw { code: 'E_NOT_FOUND', message: `Unsupported page world action: ${payload.action}` };
|
|
895
|
+
} catch (error) {
|
|
896
|
+
return {
|
|
897
|
+
url: window.location.href,
|
|
898
|
+
framePath: payload.scope === 'current' ? payload.framePath ?? [] : [],
|
|
899
|
+
error:
|
|
900
|
+
typeof error === 'object' && error !== null && 'code' in error
|
|
901
|
+
? (error as { code: string; message: string; details?: Record<string, unknown> })
|
|
902
|
+
: { code: 'E_EXECUTION', message: error instanceof Error ? error.message : String(error) }
|
|
903
|
+
};
|
|
387
904
|
}
|
|
388
|
-
} catch {
|
|
389
|
-
// Ignore transient lookup failures while the tab is navigating.
|
|
390
905
|
}
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function normalizeComparableTabUrl(url: string): string {
|
|
398
|
-
const raw = url.trim();
|
|
399
|
-
if (!raw) {
|
|
400
|
-
return raw;
|
|
401
|
-
}
|
|
402
|
-
try {
|
|
403
|
-
const parsed = new URL(raw);
|
|
404
|
-
parsed.hash = '';
|
|
405
|
-
return parsed.href;
|
|
406
|
-
} catch {
|
|
407
|
-
return raw;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
906
|
+
});
|
|
410
907
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
):
|
|
415
|
-
if (expectedUrl && expectedUrl !== 'about:blank') {
|
|
416
|
-
await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => undefined);
|
|
417
|
-
}
|
|
418
|
-
let refreshedTab = opened.tab;
|
|
419
|
-
try {
|
|
420
|
-
const rawTab = await chrome.tabs.get(opened.tab.id);
|
|
421
|
-
const pendingUrl = 'pendingUrl' in rawTab && typeof rawTab.pendingUrl === 'string' ? rawTab.pendingUrl : '';
|
|
422
|
-
const currentUrl = rawTab.url ?? '';
|
|
423
|
-
const effectiveUrl =
|
|
424
|
-
currentUrl && currentUrl !== 'about:blank'
|
|
425
|
-
? currentUrl
|
|
426
|
-
: pendingUrl && pendingUrl !== 'about:blank'
|
|
427
|
-
? pendingUrl
|
|
428
|
-
: currentUrl || pendingUrl || opened.tab.url;
|
|
429
|
-
refreshedTab = {
|
|
430
|
-
...toTabInfo(rawTab),
|
|
431
|
-
url: effectiveUrl
|
|
908
|
+
if (scope === 'all-frames') {
|
|
909
|
+
return {
|
|
910
|
+
scope,
|
|
911
|
+
results: results.map((item) => (item.result ?? { url: '', framePath: [] }) as PageFrameResult<T>)
|
|
432
912
|
};
|
|
433
|
-
} catch {
|
|
434
|
-
refreshedTab = (await workspaceBrowser.getTab(opened.tab.id)) ?? opened.tab;
|
|
435
913
|
}
|
|
436
|
-
const refreshedWorkspace = (await workspaceManager.getWorkspaceInfo(opened.workspace.id)) ?? {
|
|
437
|
-
...opened.workspace,
|
|
438
|
-
tabs: opened.workspace.tabs.map((tab) => (tab.id === refreshedTab.id ? refreshedTab : tab))
|
|
439
|
-
};
|
|
440
914
|
|
|
441
915
|
return {
|
|
442
|
-
|
|
443
|
-
|
|
916
|
+
scope,
|
|
917
|
+
result: (results[0]?.result ?? { url: '', framePath }) as PageFrameResult<T>
|
|
444
918
|
};
|
|
445
919
|
}
|
|
446
920
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
921
|
+
function truncateNetworkEntry(entry: NetworkEntry, bodyBytes?: number): NetworkEntry {
|
|
922
|
+
if (typeof bodyBytes !== 'number' || !Number.isFinite(bodyBytes) || bodyBytes <= 0) {
|
|
923
|
+
return entry;
|
|
924
|
+
}
|
|
925
|
+
const maxBytes = Math.max(1, Math.floor(bodyBytes));
|
|
926
|
+
const clone: NetworkEntry = { ...entry };
|
|
927
|
+
if (typeof clone.requestBodyPreview === 'string') {
|
|
928
|
+
const requestBytes = textEncoder.encode(clone.requestBodyPreview);
|
|
929
|
+
if (requestBytes.byteLength > maxBytes) {
|
|
930
|
+
clone.requestBodyPreview = textDecoder.decode(requestBytes.subarray(0, maxBytes));
|
|
931
|
+
clone.requestBodyTruncated = true;
|
|
932
|
+
clone.truncated = true;
|
|
456
933
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
934
|
+
}
|
|
935
|
+
if (typeof clone.responseBodyPreview === 'string') {
|
|
936
|
+
const responseBytes = textEncoder.encode(clone.responseBodyPreview);
|
|
937
|
+
if (responseBytes.byteLength > maxBytes) {
|
|
938
|
+
clone.responseBodyPreview = textDecoder.decode(responseBytes.subarray(0, maxBytes));
|
|
939
|
+
clone.responseBodyTruncated = true;
|
|
940
|
+
clone.truncated = true;
|
|
462
941
|
}
|
|
463
|
-
return tab;
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
if (typeof target.tabId === 'number') {
|
|
467
|
-
const tab = await chrome.tabs.get(target.tabId);
|
|
468
|
-
return validate(tab);
|
|
469
942
|
}
|
|
470
|
-
|
|
471
|
-
const resolved = await workspaceManager.resolveTarget({
|
|
472
|
-
tabId: target.tabId,
|
|
473
|
-
workspaceId: typeof target.workspaceId === 'string' ? target.workspaceId : undefined,
|
|
474
|
-
createIfMissing: false
|
|
475
|
-
});
|
|
476
|
-
const tab = await chrome.tabs.get(resolved.tab.id);
|
|
477
|
-
return validate(tab);
|
|
943
|
+
return clone;
|
|
478
944
|
}
|
|
479
945
|
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
|
|
946
|
+
function filterNetworkEntrySections(entry: NetworkEntry, include: unknown): NetworkEntry {
|
|
947
|
+
if (!Array.isArray(include)) {
|
|
948
|
+
return entry;
|
|
483
949
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
950
|
+
const sections = new Set(
|
|
951
|
+
include
|
|
952
|
+
.map(String)
|
|
953
|
+
.filter((section): section is 'request' | 'response' => section === 'request' || section === 'response')
|
|
954
|
+
);
|
|
955
|
+
if (sections.size === 0 || sections.size === 2) {
|
|
956
|
+
return entry;
|
|
492
957
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
958
|
+
const clone: NetworkEntry = { ...entry };
|
|
959
|
+
if (!sections.has('request')) {
|
|
960
|
+
delete clone.requestHeaders;
|
|
961
|
+
delete clone.requestBodyPreview;
|
|
962
|
+
delete clone.requestBodyTruncated;
|
|
963
|
+
}
|
|
964
|
+
if (!sections.has('response')) {
|
|
965
|
+
delete clone.responseHeaders;
|
|
966
|
+
delete clone.responseBodyPreview;
|
|
967
|
+
delete clone.responseBodyTruncated;
|
|
968
|
+
delete clone.binary;
|
|
504
969
|
}
|
|
970
|
+
return clone;
|
|
505
971
|
}
|
|
506
972
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
detail.includes('Receiving end does not exist') ||
|
|
520
|
-
detail.includes('Could not establish connection') ||
|
|
521
|
-
detail.includes('No tab with id') ||
|
|
522
|
-
detail.includes('message port closed before a response was received') ||
|
|
523
|
-
detail.includes('Content script returned undefined response');
|
|
524
|
-
if (!retriable || attempt >= maxAttempts) {
|
|
525
|
-
throw toError('E_NOT_READY', 'Content script unavailable', { detail });
|
|
526
|
-
}
|
|
527
|
-
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
|
|
973
|
+
function replayHeadersFromEntry(entry: NetworkEntry): Record<string, string> | undefined {
|
|
974
|
+
if (!entry.requestHeaders) {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
const headers: Record<string, string> = {};
|
|
978
|
+
for (const [name, value] of Object.entries(entry.requestHeaders)) {
|
|
979
|
+
const normalizedName = name.toLowerCase();
|
|
980
|
+
if (REPLAY_FORBIDDEN_HEADER_NAMES.has(normalizedName) || normalizedName.startsWith('sec-')) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (containsRedactionMarker(value)) {
|
|
984
|
+
continue;
|
|
528
985
|
}
|
|
986
|
+
headers[name] = value;
|
|
529
987
|
}
|
|
530
|
-
|
|
531
|
-
throw toError('E_NOT_READY', 'Content script unavailable');
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
interface FocusContext {
|
|
535
|
-
windowId: number | null;
|
|
536
|
-
tabId: number | null;
|
|
988
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
537
989
|
}
|
|
538
990
|
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
991
|
+
function parseTimestampCandidate(value: string, now = Date.now()): number | null {
|
|
992
|
+
const normalized = value.trim().toLowerCase();
|
|
993
|
+
if (!normalized) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
if (normalized === 'today') {
|
|
997
|
+
return now;
|
|
998
|
+
}
|
|
999
|
+
if (normalized === 'yesterday') {
|
|
1000
|
+
return now - 24 * 60 * 60 * 1000;
|
|
1001
|
+
}
|
|
1002
|
+
const parsed = Date.parse(value);
|
|
1003
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
546
1004
|
}
|
|
547
1005
|
|
|
548
|
-
|
|
549
|
-
if (
|
|
550
|
-
|
|
551
|
-
await chrome.windows.update(context.windowId, { focused: true });
|
|
552
|
-
} catch {
|
|
553
|
-
// Ignore restore errors if the original window no longer exists.
|
|
554
|
-
}
|
|
1006
|
+
function extractLatestTimestamp(values: string[] | undefined, now = Date.now()): number | null {
|
|
1007
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
1008
|
+
return null;
|
|
555
1009
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
1010
|
+
let latest: number | null = null;
|
|
1011
|
+
for (const value of values) {
|
|
1012
|
+
const parsed = parseTimestampCandidate(value, now);
|
|
1013
|
+
if (parsed === null) {
|
|
1014
|
+
continue;
|
|
561
1015
|
}
|
|
1016
|
+
latest = latest === null ? parsed : Math.max(latest, parsed);
|
|
562
1017
|
}
|
|
1018
|
+
return latest;
|
|
563
1019
|
}
|
|
564
1020
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1021
|
+
function computeFreshnessAssessment(input: {
|
|
1022
|
+
latestInlineDataTimestamp: number | null;
|
|
1023
|
+
latestNetworkTimestamp: number | null;
|
|
1024
|
+
domVisibleTimestamp: number | null;
|
|
1025
|
+
lastMutationAt: number | null;
|
|
1026
|
+
freshWindowMs: number;
|
|
1027
|
+
staleWindowMs: number;
|
|
1028
|
+
}): PageFreshnessResult['assessment'] {
|
|
1029
|
+
const now = Date.now();
|
|
1030
|
+
const latestDataTimestamp = [input.latestInlineDataTimestamp, input.domVisibleTimestamp]
|
|
1031
|
+
.filter((value): value is number => typeof value === 'number')
|
|
1032
|
+
.sort((left, right) => right - left)[0] ?? null;
|
|
1033
|
+
if (latestDataTimestamp !== null && now - latestDataTimestamp <= input.freshWindowMs) {
|
|
1034
|
+
return 'fresh';
|
|
568
1035
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
await restoreFocusContext(focusContext);
|
|
1036
|
+
const recentSignals = [input.latestNetworkTimestamp, input.lastMutationAt]
|
|
1037
|
+
.filter((value): value is number => typeof value === 'number')
|
|
1038
|
+
.some((value) => now - value <= input.freshWindowMs);
|
|
1039
|
+
if (recentSignals && latestDataTimestamp !== null && now - latestDataTimestamp > input.freshWindowMs) {
|
|
1040
|
+
return 'lagged';
|
|
575
1041
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
value: unknown
|
|
581
|
-
): { ok: boolean; result?: unknown; error?: CliResponse['error'] } {
|
|
582
|
-
if (typeof value !== 'object' || value === null || typeof (value as { ok?: unknown }).ok !== 'boolean') {
|
|
583
|
-
throw toError('E_NOT_READY', `Content script returned malformed response for ${method}`);
|
|
1042
|
+
const staleSignals = [input.latestNetworkTimestamp, input.lastMutationAt, latestDataTimestamp]
|
|
1043
|
+
.filter((value): value is number => typeof value === 'number');
|
|
1044
|
+
if (staleSignals.length > 0 && staleSignals.every((value) => now - value > input.staleWindowMs)) {
|
|
1045
|
+
return 'stale';
|
|
584
1046
|
}
|
|
585
|
-
return
|
|
1047
|
+
return 'unknown';
|
|
586
1048
|
}
|
|
587
1049
|
|
|
588
|
-
async function
|
|
589
|
-
tabId
|
|
590
|
-
method: string,
|
|
591
|
-
params: Record<string, unknown>
|
|
592
|
-
): Promise<unknown> {
|
|
593
|
-
const raw = await sendToContent<unknown>(tabId, {
|
|
594
|
-
type: 'bak.rpc',
|
|
595
|
-
method,
|
|
596
|
-
params
|
|
597
|
-
});
|
|
598
|
-
const response = requireRpcEnvelope(method, raw);
|
|
599
|
-
|
|
600
|
-
if (!response.ok) {
|
|
601
|
-
throw response.error ?? toError('E_INTERNAL', `${method} failed`);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
return response.result;
|
|
1050
|
+
async function collectPageInspection(tabId: number, params: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
1051
|
+
return (await forwardContentRpc(tabId, 'bak.internal.inspectState', params)) as Record<string, unknown>;
|
|
605
1052
|
}
|
|
606
1053
|
|
|
607
|
-
async function
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
1054
|
+
async function buildFreshnessForTab(tabId: number, params: Record<string, unknown> = {}): Promise<PageFreshnessResult> {
|
|
1055
|
+
const inspection = await collectPageInspection(tabId, params);
|
|
1056
|
+
const visibleTimestamps = Array.isArray(inspection.visibleTimestamps) ? inspection.visibleTimestamps.map(String) : [];
|
|
1057
|
+
const inlineTimestamps = Array.isArray(inspection.inlineTimestamps) ? inspection.inlineTimestamps.map(String) : [];
|
|
1058
|
+
const now = Date.now();
|
|
1059
|
+
const freshWindowMs = typeof params.freshWindowMs === 'number' ? Math.max(1, Math.floor(params.freshWindowMs)) : 15 * 60 * 1000;
|
|
1060
|
+
const staleWindowMs = typeof params.staleWindowMs === 'number' ? Math.max(freshWindowMs, Math.floor(params.staleWindowMs)) : 24 * 60 * 60 * 1000;
|
|
1061
|
+
const latestInlineDataTimestamp = extractLatestTimestamp(inlineTimestamps, now);
|
|
1062
|
+
const domVisibleTimestamp = extractLatestTimestamp(visibleTimestamps, now);
|
|
1063
|
+
const latestNetworkTs = latestNetworkTimestamp(tabId);
|
|
1064
|
+
const lastMutationAt = typeof inspection.lastMutationAt === 'number' ? inspection.lastMutationAt : null;
|
|
1065
|
+
return {
|
|
1066
|
+
pageLoadedAt: typeof inspection.pageLoadedAt === 'number' ? inspection.pageLoadedAt : null,
|
|
1067
|
+
lastMutationAt,
|
|
1068
|
+
latestNetworkTimestamp: latestNetworkTs,
|
|
1069
|
+
latestInlineDataTimestamp,
|
|
1070
|
+
domVisibleTimestamp,
|
|
1071
|
+
assessment: computeFreshnessAssessment({
|
|
1072
|
+
latestInlineDataTimestamp,
|
|
1073
|
+
latestNetworkTimestamp: latestNetworkTs,
|
|
1074
|
+
domVisibleTimestamp,
|
|
1075
|
+
lastMutationAt,
|
|
1076
|
+
freshWindowMs,
|
|
1077
|
+
staleWindowMs
|
|
1078
|
+
}),
|
|
1079
|
+
evidence: {
|
|
1080
|
+
visibleTimestamps,
|
|
1081
|
+
inlineTimestamps,
|
|
1082
|
+
networkSampleIds: recentNetworkSampleIds(tabId)
|
|
1083
|
+
}
|
|
612
1084
|
};
|
|
613
|
-
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
1088
|
+
const params = request.params ?? {};
|
|
1089
|
+
const target = {
|
|
1090
|
+
tabId: typeof params.tabId === 'number' ? params.tabId : undefined,
|
|
1091
|
+
workspaceId: typeof params.workspaceId === 'string' ? params.workspaceId : undefined
|
|
1092
|
+
};
|
|
1093
|
+
|
|
614
1094
|
const rpcForwardMethods = new Set([
|
|
615
1095
|
'page.title',
|
|
616
1096
|
'page.url',
|
|
@@ -619,542 +1099,832 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
619
1099
|
'page.accessibilityTree',
|
|
620
1100
|
'page.scrollTo',
|
|
621
1101
|
'page.metrics',
|
|
622
|
-
'element.hover',
|
|
623
|
-
'element.doubleClick',
|
|
624
|
-
'element.rightClick',
|
|
625
|
-
'element.dragDrop',
|
|
626
|
-
'element.select',
|
|
627
|
-
'element.check',
|
|
628
|
-
'element.uncheck',
|
|
629
|
-
'element.scrollIntoView',
|
|
630
|
-
'element.focus',
|
|
631
|
-
'element.blur',
|
|
632
|
-
'element.get',
|
|
633
|
-
'keyboard.press',
|
|
634
|
-
'keyboard.type',
|
|
635
|
-
'keyboard.hotkey',
|
|
636
|
-
'mouse.move',
|
|
637
|
-
'mouse.click',
|
|
638
|
-
'mouse.wheel',
|
|
639
|
-
'file.upload',
|
|
1102
|
+
'element.hover',
|
|
1103
|
+
'element.doubleClick',
|
|
1104
|
+
'element.rightClick',
|
|
1105
|
+
'element.dragDrop',
|
|
1106
|
+
'element.select',
|
|
1107
|
+
'element.check',
|
|
1108
|
+
'element.uncheck',
|
|
1109
|
+
'element.scrollIntoView',
|
|
1110
|
+
'element.focus',
|
|
1111
|
+
'element.blur',
|
|
1112
|
+
'element.get',
|
|
1113
|
+
'keyboard.press',
|
|
1114
|
+
'keyboard.type',
|
|
1115
|
+
'keyboard.hotkey',
|
|
1116
|
+
'mouse.move',
|
|
1117
|
+
'mouse.click',
|
|
1118
|
+
'mouse.wheel',
|
|
1119
|
+
'file.upload',
|
|
1120
|
+
'context.get',
|
|
1121
|
+
'context.set',
|
|
640
1122
|
'context.enterFrame',
|
|
641
1123
|
'context.exitFrame',
|
|
642
1124
|
'context.enterShadow',
|
|
643
1125
|
'context.exitShadow',
|
|
644
1126
|
'context.reset',
|
|
645
|
-
'
|
|
646
|
-
'
|
|
647
|
-
'
|
|
648
|
-
'
|
|
649
|
-
'debug.dumpState'
|
|
1127
|
+
'table.list',
|
|
1128
|
+
'table.schema',
|
|
1129
|
+
'table.rows',
|
|
1130
|
+
'table.export'
|
|
650
1131
|
]);
|
|
651
|
-
|
|
652
|
-
switch (request.method) {
|
|
653
|
-
case 'session.ping': {
|
|
654
|
-
return { ok: true, ts: Date.now() };
|
|
655
|
-
}
|
|
656
|
-
case 'tabs.list': {
|
|
657
|
-
const tabs = await chrome.tabs.query({});
|
|
658
|
-
return {
|
|
659
|
-
tabs: tabs
|
|
660
|
-
.filter((tab): tab is chrome.tabs.Tab => typeof tab.id === 'number' && typeof tab.windowId === 'number')
|
|
661
|
-
.map((tab) => toTabInfo(tab))
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
case 'tabs.getActive': {
|
|
665
|
-
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
666
|
-
const tab = tabs[0];
|
|
667
|
-
if (!tab || typeof tab.id !== 'number') {
|
|
668
|
-
return { tab: null };
|
|
669
|
-
}
|
|
670
|
-
return {
|
|
671
|
-
tab: toTabInfo(tab)
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
case 'tabs.get': {
|
|
675
|
-
const tabId = Number(params.tabId);
|
|
676
|
-
const tab = await chrome.tabs.get(tabId);
|
|
677
|
-
if (typeof tab.id !== 'number') {
|
|
678
|
-
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
679
|
-
}
|
|
680
|
-
return {
|
|
681
|
-
tab: toTabInfo(tab)
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
case 'tabs.focus': {
|
|
685
|
-
const tabId = Number(params.tabId);
|
|
686
|
-
await chrome.tabs.update(tabId, { active: true });
|
|
687
|
-
return { ok: true };
|
|
688
|
-
}
|
|
689
|
-
case 'tabs.new': {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (params.addToGroup === true && typeof tab.id === 'number') {
|
|
714
|
-
const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
|
|
715
|
-
return {
|
|
716
|
-
tabId: tab.id,
|
|
717
|
-
windowId: tab.windowId,
|
|
718
|
-
groupId
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
tabId: tab.id,
|
|
723
|
-
windowId: tab.windowId
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
case 'tabs.close': {
|
|
727
|
-
const tabId = Number(params.tabId);
|
|
728
|
-
await chrome.tabs.remove(tabId);
|
|
729
|
-
return { ok: true };
|
|
730
|
-
}
|
|
1132
|
+
|
|
1133
|
+
switch (request.method) {
|
|
1134
|
+
case 'session.ping': {
|
|
1135
|
+
return { ok: true, ts: Date.now() };
|
|
1136
|
+
}
|
|
1137
|
+
case 'tabs.list': {
|
|
1138
|
+
const tabs = await chrome.tabs.query({});
|
|
1139
|
+
return {
|
|
1140
|
+
tabs: tabs
|
|
1141
|
+
.filter((tab): tab is chrome.tabs.Tab => typeof tab.id === 'number' && typeof tab.windowId === 'number')
|
|
1142
|
+
.map((tab) => toTabInfo(tab))
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
case 'tabs.getActive': {
|
|
1146
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
1147
|
+
const tab = tabs[0];
|
|
1148
|
+
if (!tab || typeof tab.id !== 'number') {
|
|
1149
|
+
return { tab: null };
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
tab: toTabInfo(tab)
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
case 'tabs.get': {
|
|
1156
|
+
const tabId = Number(params.tabId);
|
|
1157
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1158
|
+
if (typeof tab.id !== 'number') {
|
|
1159
|
+
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
tab: toTabInfo(tab)
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
case 'tabs.focus': {
|
|
1166
|
+
const tabId = Number(params.tabId);
|
|
1167
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
1168
|
+
return { ok: true };
|
|
1169
|
+
}
|
|
1170
|
+
case 'tabs.new': {
|
|
1171
|
+
const tab = await chrome.tabs.create({
|
|
1172
|
+
url: (params.url as string | undefined) ?? 'about:blank',
|
|
1173
|
+
windowId: typeof params.windowId === 'number' ? params.windowId : undefined,
|
|
1174
|
+
active: params.active === true
|
|
1175
|
+
});
|
|
1176
|
+
if (params.addToGroup === true && typeof tab.id === 'number') {
|
|
1177
|
+
const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
|
|
1178
|
+
return {
|
|
1179
|
+
tabId: tab.id,
|
|
1180
|
+
windowId: tab.windowId,
|
|
1181
|
+
groupId
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
return {
|
|
1185
|
+
tabId: tab.id,
|
|
1186
|
+
windowId: tab.windowId
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
case 'tabs.close': {
|
|
1190
|
+
const tabId = Number(params.tabId);
|
|
1191
|
+
await chrome.tabs.remove(tabId);
|
|
1192
|
+
return { ok: true };
|
|
1193
|
+
}
|
|
731
1194
|
case 'workspace.ensure': {
|
|
732
1195
|
return preserveHumanFocus(params.focus !== true, async () => {
|
|
733
|
-
|
|
734
|
-
workspaceId:
|
|
1196
|
+
const result = await bindingManager.ensureWorkspace({
|
|
1197
|
+
workspaceId: String(params.workspaceId ?? ''),
|
|
735
1198
|
focus: params.focus === true,
|
|
736
1199
|
initialUrl: typeof params.url === 'string' ? params.url : undefined
|
|
737
1200
|
});
|
|
1201
|
+
for (const tab of result.workspace.tabs) {
|
|
1202
|
+
void ensureNetworkDebugger(tab.id).catch(() => undefined);
|
|
1203
|
+
}
|
|
1204
|
+
return result;
|
|
738
1205
|
});
|
|
739
1206
|
}
|
|
740
|
-
case 'workspace.info': {
|
|
741
|
-
return {
|
|
742
|
-
workspace: await
|
|
743
|
-
};
|
|
744
|
-
}
|
|
1207
|
+
case 'workspace.info': {
|
|
1208
|
+
return {
|
|
1209
|
+
workspace: await bindingManager.getWorkspaceInfo(String(params.workspaceId ?? ''))
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
745
1212
|
case 'workspace.openTab': {
|
|
746
1213
|
const expectedUrl = typeof params.url === 'string' ? params.url : undefined;
|
|
747
1214
|
const opened = await preserveHumanFocus(params.focus !== true, async () => {
|
|
748
|
-
return await
|
|
749
|
-
workspaceId:
|
|
750
|
-
url: expectedUrl,
|
|
751
|
-
active: params.active === true,
|
|
1215
|
+
return await bindingManager.openTab({
|
|
1216
|
+
workspaceId: String(params.workspaceId ?? ''),
|
|
1217
|
+
url: expectedUrl,
|
|
1218
|
+
active: params.active === true,
|
|
752
1219
|
focus: params.focus === true
|
|
753
1220
|
});
|
|
754
1221
|
});
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
return await workspaceManager.listTabs(typeof params.workspaceId === 'string' ? params.workspaceId : undefined);
|
|
759
|
-
}
|
|
760
|
-
case 'workspace.getActiveTab': {
|
|
761
|
-
return await workspaceManager.getActiveTab(typeof params.workspaceId === 'string' ? params.workspaceId : undefined);
|
|
1222
|
+
const finalized = await finalizeOpenedWorkspaceTab(opened, expectedUrl);
|
|
1223
|
+
void ensureNetworkDebugger(finalized.tab.id).catch(() => undefined);
|
|
1224
|
+
return finalized;
|
|
762
1225
|
}
|
|
1226
|
+
case 'workspace.listTabs': {
|
|
1227
|
+
return await bindingManager.listTabs(String(params.workspaceId ?? ''));
|
|
1228
|
+
}
|
|
1229
|
+
case 'workspace.getActiveTab': {
|
|
1230
|
+
return await bindingManager.getActiveTab(String(params.workspaceId ?? ''));
|
|
1231
|
+
}
|
|
763
1232
|
case 'workspace.setActiveTab': {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
return await workspaceManager.focus(typeof params.workspaceId === 'string' ? params.workspaceId : undefined);
|
|
768
|
-
}
|
|
769
|
-
case 'workspace.reset': {
|
|
770
|
-
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
771
|
-
return await workspaceManager.reset({
|
|
772
|
-
workspaceId: typeof params.workspaceId === 'string' ? params.workspaceId : undefined,
|
|
773
|
-
focus: params.focus === true,
|
|
774
|
-
initialUrl: typeof params.url === 'string' ? params.url : undefined
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
case 'workspace.close': {
|
|
779
|
-
return await workspaceManager.close(typeof params.workspaceId === 'string' ? params.workspaceId : undefined);
|
|
1233
|
+
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.workspaceId ?? ''));
|
|
1234
|
+
void ensureNetworkDebugger(result.tab.id).catch(() => undefined);
|
|
1235
|
+
return result;
|
|
780
1236
|
}
|
|
1237
|
+
case 'workspace.focus': {
|
|
1238
|
+
return await bindingManager.focus(String(params.workspaceId ?? ''));
|
|
1239
|
+
}
|
|
1240
|
+
case 'workspace.reset': {
|
|
1241
|
+
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
1242
|
+
return await bindingManager.reset({
|
|
1243
|
+
workspaceId: String(params.workspaceId ?? ''),
|
|
1244
|
+
focus: params.focus === true,
|
|
1245
|
+
initialUrl: typeof params.url === 'string' ? params.url : undefined
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
case 'workspace.close': {
|
|
1250
|
+
return await bindingManager.close(String(params.workspaceId ?? ''));
|
|
1251
|
+
}
|
|
781
1252
|
case 'page.goto': {
|
|
782
1253
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
783
1254
|
const tab = await withTab(target, {
|
|
784
1255
|
requireSupportedAutomationUrl: false
|
|
785
1256
|
});
|
|
1257
|
+
void ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
786
1258
|
const url = String(params.url ?? 'about:blank');
|
|
787
1259
|
await chrome.tabs.update(tab.id!, { url });
|
|
788
|
-
await waitForTabUrl(tab.id!, url);
|
|
789
|
-
await forwardContentRpc(tab.id!, 'page.url', { tabId: tab.id }).catch(() => undefined);
|
|
790
|
-
await waitForTabComplete(tab.id!, 5_000).catch(() => undefined);
|
|
791
|
-
return { ok: true };
|
|
792
|
-
});
|
|
793
|
-
}
|
|
1260
|
+
await waitForTabUrl(tab.id!, url);
|
|
1261
|
+
await forwardContentRpc(tab.id!, 'page.url', { tabId: tab.id }).catch(() => undefined);
|
|
1262
|
+
await waitForTabComplete(tab.id!, 5_000).catch(() => undefined);
|
|
1263
|
+
return { ok: true };
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
794
1266
|
case 'page.back': {
|
|
795
1267
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
796
1268
|
const tab = await withTab(target);
|
|
1269
|
+
void ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
797
1270
|
await chrome.tabs.goBack(tab.id!);
|
|
798
|
-
await waitForTabComplete(tab.id!);
|
|
799
|
-
return { ok: true };
|
|
800
|
-
});
|
|
801
|
-
}
|
|
1271
|
+
await waitForTabComplete(tab.id!);
|
|
1272
|
+
return { ok: true };
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
802
1275
|
case 'page.forward': {
|
|
803
1276
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
804
1277
|
const tab = await withTab(target);
|
|
1278
|
+
void ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
805
1279
|
await chrome.tabs.goForward(tab.id!);
|
|
806
|
-
await waitForTabComplete(tab.id!);
|
|
807
|
-
return { ok: true };
|
|
808
|
-
});
|
|
809
|
-
}
|
|
1280
|
+
await waitForTabComplete(tab.id!);
|
|
1281
|
+
return { ok: true };
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
810
1284
|
case 'page.reload': {
|
|
811
1285
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
812
1286
|
const tab = await withTab(target);
|
|
1287
|
+
void ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
813
1288
|
await chrome.tabs.reload(tab.id!);
|
|
814
|
-
await waitForTabComplete(tab.id!);
|
|
815
|
-
return { ok: true };
|
|
816
|
-
});
|
|
817
|
-
}
|
|
1289
|
+
await waitForTabComplete(tab.id!);
|
|
1290
|
+
return { ok: true };
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
818
1293
|
case 'page.viewport': {
|
|
819
1294
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
820
1295
|
const tab = await withTab(target, {
|
|
821
1296
|
requireSupportedAutomationUrl: false
|
|
822
1297
|
});
|
|
823
|
-
if (typeof tab.windowId !== 'number') {
|
|
824
|
-
throw toError('E_NOT_FOUND', 'Tab window unavailable');
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const width = typeof params.width === 'number' ? Math.max(320, Math.floor(params.width)) : undefined;
|
|
828
|
-
const height = typeof params.height === 'number' ? Math.max(320, Math.floor(params.height)) : undefined;
|
|
829
|
-
if (width || height) {
|
|
830
|
-
await chrome.windows.update(tab.windowId, {
|
|
831
|
-
width,
|
|
832
|
-
height
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const viewport = (await forwardContentRpc(tab.id!, 'page.viewport', {})) as {
|
|
837
|
-
width: number;
|
|
838
|
-
height: number;
|
|
839
|
-
devicePixelRatio: number;
|
|
840
|
-
};
|
|
841
|
-
const viewWidth = typeof width === 'number' ? width : viewport.width ?? tab.width ?? 0;
|
|
842
|
-
const viewHeight = typeof height === 'number' ? height : viewport.height ?? tab.height ?? 0;
|
|
843
|
-
return {
|
|
844
|
-
width: viewWidth,
|
|
845
|
-
height: viewHeight,
|
|
846
|
-
devicePixelRatio: viewport.devicePixelRatio
|
|
1298
|
+
if (typeof tab.windowId !== 'number') {
|
|
1299
|
+
throw toError('E_NOT_FOUND', 'Tab window unavailable');
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const width = typeof params.width === 'number' ? Math.max(320, Math.floor(params.width)) : undefined;
|
|
1303
|
+
const height = typeof params.height === 'number' ? Math.max(320, Math.floor(params.height)) : undefined;
|
|
1304
|
+
if (width || height) {
|
|
1305
|
+
await chrome.windows.update(tab.windowId, {
|
|
1306
|
+
width,
|
|
1307
|
+
height
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const viewport = (await forwardContentRpc(tab.id!, 'page.viewport', {})) as {
|
|
1312
|
+
width: number;
|
|
1313
|
+
height: number;
|
|
1314
|
+
devicePixelRatio: number;
|
|
1315
|
+
};
|
|
1316
|
+
const viewWidth = typeof width === 'number' ? width : viewport.width ?? tab.width ?? 0;
|
|
1317
|
+
const viewHeight = typeof height === 'number' ? height : viewport.height ?? tab.height ?? 0;
|
|
1318
|
+
return {
|
|
1319
|
+
width: viewWidth,
|
|
1320
|
+
height: viewHeight,
|
|
1321
|
+
devicePixelRatio: viewport.devicePixelRatio
|
|
847
1322
|
};
|
|
848
1323
|
});
|
|
849
1324
|
}
|
|
1325
|
+
case 'page.eval': {
|
|
1326
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1327
|
+
const tab = await withTab(target);
|
|
1328
|
+
return await executePageWorld(tab.id!, 'eval', params);
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
case 'page.extract': {
|
|
1332
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1333
|
+
const tab = await withTab(target);
|
|
1334
|
+
return await executePageWorld(tab.id!, 'extract', params);
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
case 'page.fetch': {
|
|
1338
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1339
|
+
const tab = await withTab(target);
|
|
1340
|
+
return await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', params);
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
850
1343
|
case 'page.snapshot': {
|
|
851
1344
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
852
1345
|
const tab = await withTab(target);
|
|
853
1346
|
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
854
|
-
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
855
|
-
}
|
|
856
|
-
const includeBase64 = params.includeBase64 !== false;
|
|
857
|
-
const config = await getConfig();
|
|
858
|
-
const elements = await sendToContent<{ elements: unknown[] }>(tab.id, {
|
|
859
|
-
type: 'bak.collectElements',
|
|
860
|
-
debugRichText: config.debugRichText
|
|
1347
|
+
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
1348
|
+
}
|
|
1349
|
+
const includeBase64 = params.includeBase64 !== false;
|
|
1350
|
+
const config = await getConfig();
|
|
1351
|
+
const elements = await sendToContent<{ elements: unknown[] }>(tab.id, {
|
|
1352
|
+
type: 'bak.collectElements',
|
|
1353
|
+
debugRichText: config.debugRichText
|
|
1354
|
+
});
|
|
1355
|
+
const imageData = await captureAlignedTabScreenshot(tab);
|
|
1356
|
+
return {
|
|
1357
|
+
imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, '') : '',
|
|
1358
|
+
elements: elements.elements,
|
|
1359
|
+
tabId: tab.id,
|
|
1360
|
+
url: tab.url ?? ''
|
|
1361
|
+
};
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
case 'element.click': {
|
|
1365
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1366
|
+
const tab = await withTab(target);
|
|
1367
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
1368
|
+
type: 'bak.performAction',
|
|
1369
|
+
action: 'click',
|
|
1370
|
+
locator: params.locator as Locator,
|
|
1371
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1372
|
+
});
|
|
1373
|
+
if (!response.ok) {
|
|
1374
|
+
throw response.error ?? toError('E_INTERNAL', 'element.click failed');
|
|
1375
|
+
}
|
|
1376
|
+
return { ok: true };
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
case 'element.type': {
|
|
1380
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1381
|
+
const tab = await withTab(target);
|
|
1382
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
1383
|
+
type: 'bak.performAction',
|
|
1384
|
+
action: 'type',
|
|
1385
|
+
locator: params.locator as Locator,
|
|
1386
|
+
text: String(params.text ?? ''),
|
|
1387
|
+
clear: Boolean(params.clear),
|
|
1388
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1389
|
+
});
|
|
1390
|
+
if (!response.ok) {
|
|
1391
|
+
throw response.error ?? toError('E_INTERNAL', 'element.type failed');
|
|
1392
|
+
}
|
|
1393
|
+
return { ok: true };
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
case 'element.scroll': {
|
|
1397
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1398
|
+
const tab = await withTab(target);
|
|
1399
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
1400
|
+
type: 'bak.performAction',
|
|
1401
|
+
action: 'scroll',
|
|
1402
|
+
locator: params.locator as Locator,
|
|
1403
|
+
dx: Number(params.dx ?? 0),
|
|
1404
|
+
dy: Number(params.dy ?? 320)
|
|
1405
|
+
});
|
|
1406
|
+
if (!response.ok) {
|
|
1407
|
+
throw response.error ?? toError('E_INTERNAL', 'element.scroll failed');
|
|
1408
|
+
}
|
|
1409
|
+
return { ok: true };
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
case 'page.wait': {
|
|
1413
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1414
|
+
const tab = await withTab(target);
|
|
1415
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
1416
|
+
type: 'bak.waitFor',
|
|
1417
|
+
mode: String(params.mode ?? 'selector'),
|
|
1418
|
+
value: String(params.value ?? ''),
|
|
1419
|
+
timeoutMs: Number(params.timeoutMs ?? 5000)
|
|
1420
|
+
});
|
|
1421
|
+
if (!response.ok) {
|
|
1422
|
+
throw response.error ?? toError('E_TIMEOUT', 'page.wait failed');
|
|
1423
|
+
}
|
|
1424
|
+
return { ok: true };
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
case 'debug.getConsole': {
|
|
1428
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1429
|
+
const tab = await withTab(target);
|
|
1430
|
+
const response = await sendToContent<{ entries: ConsoleEntry[] }>(tab.id!, {
|
|
1431
|
+
type: 'bak.getConsole',
|
|
1432
|
+
limit: Number(params.limit ?? 50)
|
|
861
1433
|
});
|
|
862
|
-
|
|
863
|
-
return {
|
|
864
|
-
imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, '') : '',
|
|
865
|
-
elements: elements.elements,
|
|
866
|
-
tabId: tab.id,
|
|
867
|
-
url: tab.url ?? ''
|
|
868
|
-
};
|
|
1434
|
+
return { entries: response.entries };
|
|
869
1435
|
});
|
|
870
1436
|
}
|
|
871
|
-
case '
|
|
1437
|
+
case 'network.list': {
|
|
872
1438
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
873
1439
|
const tab = await withTab(target);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1440
|
+
try {
|
|
1441
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
1442
|
+
return {
|
|
1443
|
+
entries: listNetworkEntries(tab.id!, {
|
|
1444
|
+
limit: typeof params.limit === 'number' ? params.limit : undefined,
|
|
1445
|
+
urlIncludes: typeof params.urlIncludes === 'string' ? params.urlIncludes : undefined,
|
|
1446
|
+
status: typeof params.status === 'number' ? params.status : undefined,
|
|
1447
|
+
method: typeof params.method === 'string' ? params.method : undefined
|
|
1448
|
+
})
|
|
1449
|
+
};
|
|
1450
|
+
} catch {
|
|
1451
|
+
return await forwardContentRpc(tab.id!, 'network.list', params);
|
|
882
1452
|
}
|
|
883
|
-
return { ok: true };
|
|
884
1453
|
});
|
|
885
1454
|
}
|
|
886
|
-
case '
|
|
1455
|
+
case 'network.get': {
|
|
887
1456
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
888
1457
|
const tab = await withTab(target);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1458
|
+
try {
|
|
1459
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
1460
|
+
const entry = getNetworkEntry(tab.id!, String(params.id ?? ''));
|
|
1461
|
+
if (!entry) {
|
|
1462
|
+
throw toError('E_NOT_FOUND', `network entry not found: ${String(params.id ?? '')}`);
|
|
1463
|
+
}
|
|
1464
|
+
const filtered = filterNetworkEntrySections(
|
|
1465
|
+
truncateNetworkEntry(entry, typeof params.bodyBytes === 'number' ? params.bodyBytes : undefined),
|
|
1466
|
+
params.include
|
|
1467
|
+
);
|
|
1468
|
+
return { entry: filtered };
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
if ((error as { code?: string } | undefined)?.code === 'E_NOT_FOUND') {
|
|
1471
|
+
throw error;
|
|
1472
|
+
}
|
|
1473
|
+
return await forwardContentRpc(tab.id!, 'network.get', params);
|
|
899
1474
|
}
|
|
900
|
-
return { ok: true };
|
|
901
1475
|
});
|
|
902
1476
|
}
|
|
903
|
-
case '
|
|
1477
|
+
case 'network.search': {
|
|
904
1478
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
905
1479
|
const tab = await withTab(target);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1480
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
1481
|
+
return {
|
|
1482
|
+
entries: searchNetworkEntries(
|
|
1483
|
+
tab.id!,
|
|
1484
|
+
String(params.pattern ?? ''),
|
|
1485
|
+
typeof params.limit === 'number' ? params.limit : 50
|
|
1486
|
+
)
|
|
1487
|
+
};
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
case 'network.waitFor': {
|
|
1491
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1492
|
+
const tab = await withTab(target);
|
|
1493
|
+
try {
|
|
1494
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
1495
|
+
} catch {
|
|
1496
|
+
return await forwardContentRpc(tab.id!, 'network.waitFor', params);
|
|
915
1497
|
}
|
|
1498
|
+
return {
|
|
1499
|
+
entry: await waitForNetworkEntry(tab.id!, {
|
|
1500
|
+
limit: 1,
|
|
1501
|
+
urlIncludes: typeof params.urlIncludes === 'string' ? params.urlIncludes : undefined,
|
|
1502
|
+
status: typeof params.status === 'number' ? params.status : undefined,
|
|
1503
|
+
method: typeof params.method === 'string' ? params.method : undefined,
|
|
1504
|
+
timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : undefined
|
|
1505
|
+
})
|
|
1506
|
+
};
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
case 'network.clear': {
|
|
1510
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1511
|
+
const tab = await withTab(target);
|
|
1512
|
+
clearNetworkEntries(tab.id!);
|
|
1513
|
+
await forwardContentRpc(tab.id!, 'network.clear', params).catch(() => undefined);
|
|
916
1514
|
return { ok: true };
|
|
917
1515
|
});
|
|
918
1516
|
}
|
|
919
|
-
case '
|
|
1517
|
+
case 'network.replay': {
|
|
920
1518
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
921
1519
|
const tab = await withTab(target);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1520
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
1521
|
+
const entry = getNetworkEntry(tab.id!, String(params.id ?? ''));
|
|
1522
|
+
if (!entry) {
|
|
1523
|
+
throw toError('E_NOT_FOUND', `network entry not found: ${String(params.id ?? '')}`);
|
|
1524
|
+
}
|
|
1525
|
+
if (entry.requestBodyTruncated === true) {
|
|
1526
|
+
throw toError('E_BODY_TOO_LARGE', 'captured request body was truncated and cannot be replayed safely', {
|
|
1527
|
+
requestId: entry.id,
|
|
1528
|
+
requestBytes: entry.requestBytes
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
if (containsRedactionMarker(entry.requestBodyPreview)) {
|
|
1532
|
+
throw toError('E_EXECUTION', 'captured request body was redacted and cannot be replayed safely', {
|
|
1533
|
+
requestId: entry.id
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
const replayed = await executePageWorld<PageFetchResponse>(tab.id!, 'fetch', {
|
|
1537
|
+
url: entry.url,
|
|
1538
|
+
method: entry.method,
|
|
1539
|
+
headers: replayHeadersFromEntry(entry),
|
|
1540
|
+
body: entry.requestBodyPreview,
|
|
1541
|
+
contentType: (() => {
|
|
1542
|
+
const requestHeaders = entry.requestHeaders ?? {};
|
|
1543
|
+
const contentTypeHeader = Object.keys(requestHeaders).find((name) => name.toLowerCase() === 'content-type');
|
|
1544
|
+
return contentTypeHeader ? requestHeaders[contentTypeHeader] : undefined;
|
|
1545
|
+
})(),
|
|
1546
|
+
mode: params.mode,
|
|
1547
|
+
timeoutMs: params.timeoutMs,
|
|
1548
|
+
maxBytes: params.maxBytes,
|
|
1549
|
+
scope: 'current'
|
|
927
1550
|
});
|
|
928
|
-
|
|
929
|
-
|
|
1551
|
+
const frameResult = replayed.result ?? replayed.results?.find((candidate) => candidate.value || candidate.error);
|
|
1552
|
+
if (frameResult?.error) {
|
|
1553
|
+
throw toError(frameResult.error.code ?? 'E_EXECUTION', frameResult.error.message, frameResult.error.details);
|
|
930
1554
|
}
|
|
931
|
-
|
|
1555
|
+
const first = frameResult?.value;
|
|
1556
|
+
if (!first) {
|
|
1557
|
+
throw toError('E_EXECUTION', 'network replay returned no response payload');
|
|
1558
|
+
}
|
|
1559
|
+
return first;
|
|
932
1560
|
});
|
|
933
1561
|
}
|
|
934
|
-
case '
|
|
1562
|
+
case 'page.freshness': {
|
|
935
1563
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
936
1564
|
const tab = await withTab(target);
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
limit: Number(params.limit ?? 50)
|
|
940
|
-
});
|
|
941
|
-
return { entries: response.entries };
|
|
1565
|
+
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
1566
|
+
return await buildFreshnessForTab(tab.id!, params);
|
|
942
1567
|
});
|
|
943
1568
|
}
|
|
944
|
-
case '
|
|
1569
|
+
case 'debug.dumpState': {
|
|
945
1570
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
946
1571
|
const tab = await withTab(target);
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1572
|
+
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
1573
|
+
const dump = (await forwardContentRpc(tab.id!, 'debug.dumpState', params)) as Record<string, unknown>;
|
|
1574
|
+
const inspection = await collectPageInspection(tab.id!, params);
|
|
1575
|
+
const network = listNetworkEntries(tab.id!, {
|
|
1576
|
+
limit: typeof params.networkLimit === 'number' ? params.networkLimit : 80
|
|
1577
|
+
});
|
|
1578
|
+
const sections = Array.isArray(params.section) ? new Set(params.section.map(String) as DebugDumpSection[]) : null;
|
|
1579
|
+
const result: Record<string, unknown> = {
|
|
1580
|
+
...dump,
|
|
1581
|
+
network,
|
|
1582
|
+
scripts: inspection.scripts,
|
|
1583
|
+
globalsPreview: inspection.globalsPreview,
|
|
1584
|
+
storage: inspection.storage,
|
|
1585
|
+
frames: inspection.frames,
|
|
1586
|
+
networkSummary: {
|
|
1587
|
+
total: network.length,
|
|
1588
|
+
recent: network.slice(0, Math.min(10, network.length))
|
|
952
1589
|
}
|
|
953
|
-
|
|
954
|
-
if (!
|
|
955
|
-
|
|
1590
|
+
};
|
|
1591
|
+
if (!sections || sections.size === 0) {
|
|
1592
|
+
return result;
|
|
1593
|
+
}
|
|
1594
|
+
const filtered: Record<string, unknown> = {
|
|
1595
|
+
url: result.url,
|
|
1596
|
+
title: result.title,
|
|
1597
|
+
context: result.context
|
|
1598
|
+
};
|
|
1599
|
+
if (sections.has('dom')) {
|
|
1600
|
+
filtered.dom = result.dom;
|
|
1601
|
+
}
|
|
1602
|
+
if (sections.has('visible-text')) {
|
|
1603
|
+
filtered.text = result.text;
|
|
1604
|
+
filtered.elements = result.elements;
|
|
1605
|
+
}
|
|
1606
|
+
if (sections.has('scripts')) {
|
|
1607
|
+
filtered.scripts = result.scripts;
|
|
1608
|
+
}
|
|
1609
|
+
if (sections.has('globals-preview')) {
|
|
1610
|
+
filtered.globalsPreview = result.globalsPreview;
|
|
1611
|
+
}
|
|
1612
|
+
if (sections.has('network-summary')) {
|
|
1613
|
+
filtered.networkSummary = result.networkSummary;
|
|
956
1614
|
}
|
|
957
|
-
|
|
1615
|
+
if (sections.has('storage')) {
|
|
1616
|
+
filtered.storage = result.storage;
|
|
1617
|
+
}
|
|
1618
|
+
if (sections.has('frames')) {
|
|
1619
|
+
filtered.frames = result.frames;
|
|
1620
|
+
}
|
|
1621
|
+
if (params.includeAccessibility === true && 'accessibility' in result) {
|
|
1622
|
+
filtered.accessibility = result.accessibility;
|
|
1623
|
+
}
|
|
1624
|
+
if ('snapshot' in result) {
|
|
1625
|
+
filtered.snapshot = result.snapshot;
|
|
1626
|
+
}
|
|
1627
|
+
return filtered;
|
|
958
1628
|
});
|
|
959
1629
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (reconnectTimer !== null) {
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
983
|
-
reconnectAttempt += 1;
|
|
984
|
-
nextReconnectInMs = delayMs;
|
|
985
|
-
reconnectTimer = setTimeout(() => {
|
|
986
|
-
reconnectTimer = null;
|
|
987
|
-
nextReconnectInMs = null;
|
|
988
|
-
void connectWebSocket();
|
|
989
|
-
}, delayMs) as unknown as number;
|
|
990
|
-
|
|
991
|
-
if (!lastError) {
|
|
992
|
-
setRuntimeError(`Reconnect scheduled: ${reason}`, 'socket');
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
async function connectWebSocket(): Promise<void> {
|
|
997
|
-
clearReconnectTimer();
|
|
998
|
-
if (manualDisconnect) {
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
const config = await getConfig();
|
|
1007
|
-
if (!config.token) {
|
|
1008
|
-
setRuntimeError('Pair token is empty', 'config');
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
1013
|
-
ws = new WebSocket(url);
|
|
1014
|
-
|
|
1015
|
-
ws.addEventListener('open', () => {
|
|
1016
|
-
manualDisconnect = false;
|
|
1017
|
-
reconnectAttempt = 0;
|
|
1018
|
-
lastError = null;
|
|
1019
|
-
ws?.send(JSON.stringify({
|
|
1020
|
-
type: 'hello',
|
|
1021
|
-
role: 'extension',
|
|
1022
|
-
version: '0.3.8',
|
|
1023
|
-
ts: Date.now()
|
|
1024
|
-
}));
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
ws.addEventListener('message', (event) => {
|
|
1028
|
-
try {
|
|
1029
|
-
const request = JSON.parse(String(event.data)) as CliRequest;
|
|
1030
|
-
if (!request.id || !request.method) {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
void handleRequest(request)
|
|
1034
|
-
.then((result) => {
|
|
1035
|
-
sendResponse({ id: request.id, ok: true, result });
|
|
1036
|
-
})
|
|
1037
|
-
.catch((error: unknown) => {
|
|
1038
|
-
const normalized = normalizeUnhandledError(error);
|
|
1039
|
-
sendResponse({ id: request.id, ok: false, error: normalized });
|
|
1040
|
-
});
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
setRuntimeError(error instanceof Error ? error.message : String(error), 'parse');
|
|
1043
|
-
sendResponse({
|
|
1044
|
-
id: 'parse-error',
|
|
1045
|
-
ok: false,
|
|
1046
|
-
error: toError('E_INTERNAL', error instanceof Error ? error.message : String(error))
|
|
1630
|
+
case 'inspect.pageData': {
|
|
1631
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1632
|
+
const tab = await withTab(target);
|
|
1633
|
+
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
1634
|
+
const inspection = await collectPageInspection(tab.id!, params);
|
|
1635
|
+
const network = listNetworkEntries(tab.id!, { limit: 10 });
|
|
1636
|
+
return {
|
|
1637
|
+
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
1638
|
+
tables: inspection.tables ?? [],
|
|
1639
|
+
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
1640
|
+
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
1641
|
+
recentNetwork: network,
|
|
1642
|
+
recommendedNextSteps: [
|
|
1643
|
+
'bak page extract --path table_data',
|
|
1644
|
+
'bak network search --pattern table_data',
|
|
1645
|
+
'bak page freshness'
|
|
1646
|
+
]
|
|
1647
|
+
};
|
|
1047
1648
|
});
|
|
1048
1649
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1063
|
-
void loadWorkspaceState().then(async (state) => {
|
|
1064
|
-
if (!state || !state.tabIds.includes(tabId)) {
|
|
1065
|
-
return;
|
|
1650
|
+
case 'inspect.liveUpdates': {
|
|
1651
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1652
|
+
const tab = await withTab(target);
|
|
1653
|
+
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
1654
|
+
const inspection = await collectPageInspection(tab.id!, params);
|
|
1655
|
+
const network = listNetworkEntries(tab.id!, { limit: 25 });
|
|
1656
|
+
return {
|
|
1657
|
+
lastMutationAt: inspection.lastMutationAt ?? null,
|
|
1658
|
+
timers: inspection.timers ?? { timeouts: 0, intervals: 0 },
|
|
1659
|
+
networkCount: network.length,
|
|
1660
|
+
recentNetwork: network.slice(0, 10)
|
|
1661
|
+
};
|
|
1662
|
+
});
|
|
1066
1663
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (!state || state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
1080
|
-
return;
|
|
1664
|
+
case 'inspect.freshness': {
|
|
1665
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1666
|
+
const tab = await withTab(target);
|
|
1667
|
+
const freshness = await buildFreshnessForTab(tab.id!, params);
|
|
1668
|
+
return {
|
|
1669
|
+
...freshness,
|
|
1670
|
+
lagMs:
|
|
1671
|
+
typeof freshness.latestNetworkTimestamp === 'number' && typeof freshness.latestInlineDataTimestamp === 'number'
|
|
1672
|
+
? Math.max(0, freshness.latestNetworkTimestamp - freshness.latestInlineDataTimestamp)
|
|
1673
|
+
: null
|
|
1674
|
+
};
|
|
1675
|
+
});
|
|
1081
1676
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1677
|
+
case 'capture.snapshot': {
|
|
1678
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1679
|
+
const tab = await withTab(target);
|
|
1680
|
+
await ensureNetworkDebugger(tab.id!).catch(() => undefined);
|
|
1681
|
+
const inspection = await collectPageInspection(tab.id!, params);
|
|
1682
|
+
return {
|
|
1683
|
+
url: inspection.url ?? tab.url ?? '',
|
|
1684
|
+
title: inspection.title ?? tab.title ?? '',
|
|
1685
|
+
html: inspection.html ?? '',
|
|
1686
|
+
visibleText: inspection.visibleText ?? [],
|
|
1687
|
+
cookies: inspection.cookies ?? [],
|
|
1688
|
+
storage: inspection.storage ?? { localStorageKeys: [], sessionStorageKeys: [] },
|
|
1689
|
+
context: inspection.context ?? { tabId: tab.id, framePath: [], shadowPath: [] },
|
|
1690
|
+
freshness: await buildFreshnessForTab(tab.id!, params),
|
|
1691
|
+
network: listNetworkEntries(tab.id!, {
|
|
1692
|
+
limit: typeof params.networkLimit === 'number' ? params.networkLimit : 20
|
|
1693
|
+
}),
|
|
1694
|
+
capturedAt: Date.now()
|
|
1695
|
+
};
|
|
1696
|
+
});
|
|
1093
1697
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
});
|
|
1102
|
-
});
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
chrome.runtime.onInstalled.addListener(() => {
|
|
1106
|
-
void setConfig({ port: DEFAULT_PORT, debugRichText: false });
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
chrome.runtime.onStartup.addListener(() => {
|
|
1110
|
-
void connectWebSocket();
|
|
1111
|
-
});
|
|
1112
|
-
|
|
1113
|
-
void connectWebSocket();
|
|
1114
|
-
|
|
1115
|
-
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1116
|
-
if (message?.type === 'bak.updateConfig') {
|
|
1117
|
-
manualDisconnect = false;
|
|
1118
|
-
void setConfig({
|
|
1119
|
-
token: message.token,
|
|
1120
|
-
port: Number(message.port ?? DEFAULT_PORT),
|
|
1121
|
-
debugRichText: message.debugRichText === true
|
|
1122
|
-
}).then(() => {
|
|
1123
|
-
ws?.close();
|
|
1124
|
-
void connectWebSocket().then(() => sendResponse({ ok: true }));
|
|
1125
|
-
});
|
|
1126
|
-
return true;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
if (message?.type === 'bak.getState') {
|
|
1130
|
-
void getConfig().then((config) => {
|
|
1131
|
-
sendResponse({
|
|
1132
|
-
ok: true,
|
|
1133
|
-
connected: ws?.readyState === WebSocket.OPEN,
|
|
1134
|
-
hasToken: Boolean(config.token),
|
|
1135
|
-
port: config.port,
|
|
1136
|
-
debugRichText: config.debugRichText,
|
|
1137
|
-
lastError: lastError?.message ?? null,
|
|
1138
|
-
lastErrorAt: lastError?.at ?? null,
|
|
1139
|
-
lastErrorContext: lastError?.context ?? null,
|
|
1140
|
-
reconnectAttempt,
|
|
1141
|
-
nextReconnectInMs
|
|
1698
|
+
case 'capture.har': {
|
|
1699
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1700
|
+
const tab = await withTab(target);
|
|
1701
|
+
await ensureTabNetworkCapture(tab.id!);
|
|
1702
|
+
return {
|
|
1703
|
+
har: exportHar(tab.id!, typeof params.limit === 'number' ? params.limit : undefined)
|
|
1704
|
+
};
|
|
1142
1705
|
});
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1706
|
+
}
|
|
1707
|
+
case 'ui.selectCandidate': {
|
|
1708
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1709
|
+
const tab = await withTab(target);
|
|
1710
|
+
const response = await sendToContent<{ ok: boolean; selectedEid?: string; error?: CliResponse['error'] }>(
|
|
1711
|
+
tab.id!,
|
|
1712
|
+
{
|
|
1713
|
+
type: 'bak.selectCandidate',
|
|
1714
|
+
candidates: params.candidates
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
if (!response.ok || !response.selectedEid) {
|
|
1718
|
+
throw response.error ?? toError('E_NEED_USER_CONFIRM', 'User did not confirm candidate');
|
|
1719
|
+
}
|
|
1720
|
+
return { selectedEid: response.selectedEid };
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
default:
|
|
1724
|
+
if (rpcForwardMethods.has(request.method)) {
|
|
1725
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
1726
|
+
const tab = await withTab(target);
|
|
1727
|
+
return await forwardContentRpc(tab.id!, request.method, {
|
|
1728
|
+
...params,
|
|
1729
|
+
tabId: tab.id
|
|
1730
|
+
});
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
throw toError('E_NOT_FOUND', `Unsupported method from CLI bridge: ${request.method}`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function scheduleReconnect(reason: string): void {
|
|
1738
|
+
if (manualDisconnect) {
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (reconnectTimer !== null) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
1746
|
+
reconnectAttempt += 1;
|
|
1747
|
+
nextReconnectInMs = delayMs;
|
|
1748
|
+
reconnectTimer = setTimeout(() => {
|
|
1749
|
+
reconnectTimer = null;
|
|
1750
|
+
nextReconnectInMs = null;
|
|
1751
|
+
void connectWebSocket();
|
|
1752
|
+
}, delayMs) as unknown as number;
|
|
1753
|
+
|
|
1754
|
+
if (!lastError) {
|
|
1755
|
+
setRuntimeError(`Reconnect scheduled: ${reason}`, 'socket');
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
async function connectWebSocket(): Promise<void> {
|
|
1760
|
+
clearReconnectTimer();
|
|
1761
|
+
if (manualDisconnect) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const config = await getConfig();
|
|
1770
|
+
if (!config.token) {
|
|
1771
|
+
setRuntimeError('Pair token is empty', 'config');
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
1776
|
+
ws = new WebSocket(url);
|
|
1777
|
+
|
|
1778
|
+
ws.addEventListener('open', () => {
|
|
1779
|
+
manualDisconnect = false;
|
|
1780
|
+
reconnectAttempt = 0;
|
|
1781
|
+
lastError = null;
|
|
1782
|
+
ws?.send(JSON.stringify({
|
|
1783
|
+
type: 'hello',
|
|
1784
|
+
role: 'extension',
|
|
1785
|
+
version: '0.6.0',
|
|
1786
|
+
ts: Date.now()
|
|
1787
|
+
}));
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
ws.addEventListener('message', (event) => {
|
|
1791
|
+
try {
|
|
1792
|
+
const request = JSON.parse(String(event.data)) as CliRequest;
|
|
1793
|
+
if (!request.id || !request.method) {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
void handleRequest(request)
|
|
1797
|
+
.then((result) => {
|
|
1798
|
+
sendResponse({ id: request.id, ok: true, result });
|
|
1799
|
+
})
|
|
1800
|
+
.catch((error: unknown) => {
|
|
1801
|
+
const normalized = normalizeUnhandledError(error);
|
|
1802
|
+
sendResponse({ id: request.id, ok: false, error: normalized });
|
|
1803
|
+
});
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
setRuntimeError(error instanceof Error ? error.message : String(error), 'parse');
|
|
1806
|
+
sendResponse({
|
|
1807
|
+
id: 'parse-error',
|
|
1808
|
+
ok: false,
|
|
1809
|
+
error: toError('E_INTERNAL', error instanceof Error ? error.message : String(error))
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
ws.addEventListener('close', () => {
|
|
1815
|
+
ws = null;
|
|
1816
|
+
scheduleReconnect('socket-closed');
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
ws.addEventListener('error', () => {
|
|
1820
|
+
setRuntimeError('Cannot connect to bak cli', 'socket');
|
|
1821
|
+
ws?.close();
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1826
|
+
dropNetworkCapture(tabId);
|
|
1827
|
+
void listWorkspaceStates().then(async (states) => {
|
|
1828
|
+
for (const state of states) {
|
|
1829
|
+
if (!state.tabIds.includes(tabId)) {
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
1833
|
+
await saveWorkspaceState({
|
|
1834
|
+
...state,
|
|
1835
|
+
tabIds: nextTabIds,
|
|
1836
|
+
activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
|
|
1837
|
+
primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
1844
|
+
void listWorkspaceStates().then(async (states) => {
|
|
1845
|
+
for (const state of states) {
|
|
1846
|
+
if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
await saveWorkspaceState({
|
|
1850
|
+
...state,
|
|
1851
|
+
activeTabId: activeInfo.tabId
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
1858
|
+
void listWorkspaceStates().then(async (states) => {
|
|
1859
|
+
for (const state of states) {
|
|
1860
|
+
if (state.windowId !== windowId) {
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
await saveWorkspaceState({
|
|
1864
|
+
...state,
|
|
1865
|
+
windowId: null,
|
|
1866
|
+
groupId: null,
|
|
1867
|
+
tabIds: [],
|
|
1868
|
+
activeTabId: null,
|
|
1869
|
+
primaryTabId: null
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1876
|
+
void setConfig({ port: DEFAULT_PORT, debugRichText: false });
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
1880
|
+
void connectWebSocket();
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
void connectWebSocket();
|
|
1884
|
+
|
|
1885
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1886
|
+
if (message?.type === 'bak.updateConfig') {
|
|
1887
|
+
manualDisconnect = false;
|
|
1888
|
+
void setConfig({
|
|
1889
|
+
token: message.token,
|
|
1890
|
+
port: Number(message.port ?? DEFAULT_PORT),
|
|
1891
|
+
debugRichText: message.debugRichText === true
|
|
1892
|
+
}).then(() => {
|
|
1893
|
+
ws?.close();
|
|
1894
|
+
void connectWebSocket().then(() => sendResponse({ ok: true }));
|
|
1895
|
+
});
|
|
1896
|
+
return true;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
if (message?.type === 'bak.getState') {
|
|
1900
|
+
void getConfig().then((config) => {
|
|
1901
|
+
sendResponse({
|
|
1902
|
+
ok: true,
|
|
1903
|
+
connected: ws?.readyState === WebSocket.OPEN,
|
|
1904
|
+
hasToken: Boolean(config.token),
|
|
1905
|
+
port: config.port,
|
|
1906
|
+
debugRichText: config.debugRichText,
|
|
1907
|
+
lastError: lastError?.message ?? null,
|
|
1908
|
+
lastErrorAt: lastError?.at ?? null,
|
|
1909
|
+
lastErrorContext: lastError?.context ?? null,
|
|
1910
|
+
reconnectAttempt,
|
|
1911
|
+
nextReconnectInMs
|
|
1912
|
+
});
|
|
1913
|
+
});
|
|
1914
|
+
return true;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (message?.type === 'bak.disconnect') {
|
|
1918
|
+
manualDisconnect = true;
|
|
1919
|
+
clearReconnectTimer();
|
|
1920
|
+
reconnectAttempt = 0;
|
|
1921
|
+
ws?.close();
|
|
1922
|
+
ws = null;
|
|
1923
|
+
sendResponse({ ok: true });
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
return false;
|
|
1158
1928
|
});
|
|
1159
1929
|
|
|
1160
1930
|
|