@deadragdoll/tellymcp 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-ru.md +19 -0
- package/README.md +19 -0
- package/TOOLS.md +206 -3
- package/dist/cli.js +109 -1
- package/dist/services/features/telegram-mcp/browser.service.js +38 -1
- package/dist/services/features/telegram-mcp/mcp-server.service.js +12 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +14 -0
- package/dist/services/features/telegram-mcp/src/app/config/env.js +13 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +147 -2
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +485 -1
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +502 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +79 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +559 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
- package/docs/STANDALONE-ru.md +42 -6
- package/docs/STANDALONE.md +42 -6
- package/package.json +6 -3
- package/packages/chrome-attach-extension/dist/background.js +1326 -0
- package/packages/chrome-attach-extension/dist/icon.svg +6 -0
- package/packages/chrome-attach-extension/dist/manifest.json +36 -0
- package/packages/chrome-attach-extension/dist/options.html +312 -0
- package/packages/chrome-attach-extension/dist/options.js +593 -0
- package/packages/chrome-attach-extension/dist/popup.html +93 -0
- package/packages/chrome-attach-extension/dist/popup.js +79 -0
- package/packages/chrome-attach-extension/dist/recorder-content.js +83 -0
- package/packages/chrome-attach-extension/dist/recorder-page.js +266 -0
- package/packages/firefox-attach-extension/README.md +13 -0
- package/packages/firefox-attach-extension/dist/background.js +1242 -0
- package/packages/firefox-attach-extension/dist/icon.svg +6 -0
- package/packages/firefox-attach-extension/dist/manifest.json +56 -0
- package/packages/firefox-attach-extension/dist/options.html +312 -0
- package/packages/firefox-attach-extension/dist/options.js +527 -0
- package/packages/firefox-attach-extension/dist/popup.html +93 -0
- package/packages/firefox-attach-extension/dist/popup.js +64 -0
- package/packages/firefox-attach-extension/dist/recorder-content.js +77 -0
- package/packages/firefox-attach-extension/dist/recorder-page.js +302 -0
|
@@ -0,0 +1,1326 @@
|
|
|
1
|
+
const browser = globalThis.browser ?? globalThis.chrome;
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SETTINGS = {
|
|
4
|
+
host: "127.0.0.1",
|
|
5
|
+
port: 9999,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const CONNECTION_STATUS_KEY = "attach_connection_status";
|
|
9
|
+
const INSTANCE_ID_KEY = "attach_instance_id";
|
|
10
|
+
const ATTACHED_TAB_KEY = "attach_selected_tab";
|
|
11
|
+
const CONNECTION_ENABLED_KEY = "attach_connection_enabled";
|
|
12
|
+
const RECORDING_STATUS_KEY = "attach_recording_status";
|
|
13
|
+
const POPUP_COMMAND_KEY = "attach_popup_command";
|
|
14
|
+
const POPUP_COMMAND_RESULT_KEY = "attach_popup_command_result";
|
|
15
|
+
const CONTROL_PANEL_PORT_NAME = "telly-control-panel";
|
|
16
|
+
const RECONNECT_DELAY_MS = 3000;
|
|
17
|
+
const HEARTBEAT_INTERVAL_MS = 15000;
|
|
18
|
+
const MAX_CAPTURE_BYTES = 512 * 1024;
|
|
19
|
+
|
|
20
|
+
let socket = null;
|
|
21
|
+
let reconnectTimer = null;
|
|
22
|
+
let heartbeatTimer = null;
|
|
23
|
+
let instanceId = null;
|
|
24
|
+
let manualDisconnect = false;
|
|
25
|
+
const pendingManualRecordingRequests = new Map();
|
|
26
|
+
const controlPanelPorts = new Set();
|
|
27
|
+
|
|
28
|
+
const activeRecordingsById = new Map();
|
|
29
|
+
const activeRecordingIdByTabId = new Map();
|
|
30
|
+
|
|
31
|
+
function storageGet(defaults) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
browser.storage.local.get(defaults, (result) => {
|
|
34
|
+
const error = browser.runtime?.lastError;
|
|
35
|
+
if (error) {
|
|
36
|
+
reject(new Error(error.message));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
resolve(result);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function storageSet(value) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
browser.storage.local.set(value, () => {
|
|
47
|
+
const error = browser.runtime?.lastError;
|
|
48
|
+
if (error) {
|
|
49
|
+
reject(new Error(error.message));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tabsQuery(queryInfo) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
browser.tabs.query(queryInfo, (tabs) => {
|
|
60
|
+
const error = browser.runtime?.lastError;
|
|
61
|
+
if (error) {
|
|
62
|
+
reject(new Error(error.message));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve(tabs || []);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function tabsGet(tabId) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
browser.tabs.get(tabId, (tab) => {
|
|
73
|
+
const error = browser.runtime?.lastError;
|
|
74
|
+
if (error) {
|
|
75
|
+
reject(new Error(error.message));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
resolve(tab);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function tabsUpdate(tabId, updateProperties) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
browser.tabs.update(tabId, updateProperties, (tab) => {
|
|
86
|
+
const error = browser.runtime?.lastError;
|
|
87
|
+
if (error) {
|
|
88
|
+
reject(new Error(error.message));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
resolve(tab);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function windowsUpdate(windowId, updateInfo) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
browser.windows.update(windowId, updateInfo, (window) => {
|
|
99
|
+
const error = browser.runtime?.lastError;
|
|
100
|
+
if (error) {
|
|
101
|
+
reject(new Error(error.message));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
resolve(window);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cookiesGetAll(details) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
browser.cookies.getAll(details, (cookies) => {
|
|
112
|
+
const error = browser.runtime?.lastError;
|
|
113
|
+
if (error) {
|
|
114
|
+
reject(new Error(error.message));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
resolve(cookies || []);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function captureVisibleTab(windowId, options) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
browser.tabs.captureVisibleTab(windowId, options, (dataUrl) => {
|
|
125
|
+
const error = browser.runtime?.lastError;
|
|
126
|
+
if (error) {
|
|
127
|
+
reject(new Error(error.message));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
resolve(dataUrl);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function scriptingExecuteScript(options) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
browser.scripting.executeScript(options, (result) => {
|
|
138
|
+
const error = browser.runtime?.lastError;
|
|
139
|
+
if (error) {
|
|
140
|
+
reject(new Error(error.message));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
resolve(result || []);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function collectInjectExportNames(source) {
|
|
149
|
+
const names = new Set();
|
|
150
|
+
const patterns = [
|
|
151
|
+
/\basync\s+function\s+([A-Za-z_$][\w$]*)\s*\(/gu,
|
|
152
|
+
/\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/gu,
|
|
153
|
+
/\bclass\s+([A-Za-z_$][\w$]*)\s*(?:\{|extends\b)/gu,
|
|
154
|
+
/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=/gu,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const pattern of patterns) {
|
|
158
|
+
for (const match of source.matchAll(pattern)) {
|
|
159
|
+
const name = match[1];
|
|
160
|
+
if (name && name !== "TELLY") {
|
|
161
|
+
names.add(name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return [...names];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function runInjectedMainWorldScript(namespace, source, exportNames) {
|
|
170
|
+
try {
|
|
171
|
+
window[namespace] = window[namespace] || {};
|
|
172
|
+
const wrapped = "window[" + JSON.stringify(namespace) + "]=window[" + JSON.stringify(namespace) + "]||{};var TELLY=window[" + JSON.stringify(namespace) + "];\n"
|
|
173
|
+
+ String(source || "");
|
|
174
|
+
|
|
175
|
+
const script = document.createElement("script");
|
|
176
|
+
script.textContent = wrapped;
|
|
177
|
+
(document.documentElement || document.head || document.body).appendChild(script);
|
|
178
|
+
script.remove();
|
|
179
|
+
|
|
180
|
+
for (const name of Array.isArray(exportNames) ? exportNames : []) {
|
|
181
|
+
if (!name || !(name in window)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
window[namespace][name] = window[name];
|
|
186
|
+
} catch {
|
|
187
|
+
// ignore unassignable globals
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
result: {
|
|
194
|
+
namespace,
|
|
195
|
+
bytes: String(source || "").length,
|
|
196
|
+
url: location.href,
|
|
197
|
+
title: document.title,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: error instanceof Error ? error.message : String(error),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function getSettings() {
|
|
209
|
+
return await storageGet({
|
|
210
|
+
...DEFAULT_SETTINGS,
|
|
211
|
+
[CONNECTION_ENABLED_KEY]: true,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function setConnectionStatus(status) {
|
|
216
|
+
await storageSet({
|
|
217
|
+
[CONNECTION_STATUS_KEY]: status,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function setLocalInstanceId(value) {
|
|
222
|
+
await storageSet({
|
|
223
|
+
[INSTANCE_ID_KEY]: value,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function setAttachedTab(tab) {
|
|
228
|
+
await storageSet({
|
|
229
|
+
[ATTACHED_TAB_KEY]: tab,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function setRecordingStatus(status) {
|
|
234
|
+
await storageSet({
|
|
235
|
+
[RECORDING_STATUS_KEY]: status,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildWebSocketUrl(settings) {
|
|
240
|
+
return `ws://${settings.host}:${settings.port}/browser-attach/ws`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function computeInstanceId() {
|
|
244
|
+
if (instanceId) {
|
|
245
|
+
return instanceId;
|
|
246
|
+
}
|
|
247
|
+
const runtimeId = browser.runtime.id || "chrome";
|
|
248
|
+
instanceId = `chrome-${runtimeId}`;
|
|
249
|
+
await setLocalInstanceId(instanceId);
|
|
250
|
+
return instanceId;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function listTabs() {
|
|
254
|
+
const tabs = await tabsQuery({});
|
|
255
|
+
return tabs.map((tab) => ({
|
|
256
|
+
tab_id: tab.id,
|
|
257
|
+
window_id: tab.windowId,
|
|
258
|
+
active: tab.active === true,
|
|
259
|
+
title: tab.title || "",
|
|
260
|
+
url: tab.url || "",
|
|
261
|
+
status: tab.status || "",
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function getActiveTab() {
|
|
266
|
+
const tabs = await tabsQuery({ active: true, currentWindow: true });
|
|
267
|
+
const tab = tabs[0];
|
|
268
|
+
if (!tab || typeof tab.id !== "number") {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
tab_id: tab.id,
|
|
273
|
+
window_id: tab.windowId,
|
|
274
|
+
active: tab.active === true,
|
|
275
|
+
title: tab.title || "",
|
|
276
|
+
url: tab.url || "",
|
|
277
|
+
status: tab.status || "",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function ensureTabIsActive(tabId) {
|
|
282
|
+
let tab;
|
|
283
|
+
try {
|
|
284
|
+
tab = await tabsGet(tabId);
|
|
285
|
+
} catch {
|
|
286
|
+
throw new Error("Tab not found.");
|
|
287
|
+
}
|
|
288
|
+
await tabsUpdate(tabId, { active: true });
|
|
289
|
+
await windowsUpdate(tab.windowId, { focused: true });
|
|
290
|
+
return await tabsGet(tabId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function buildTabActionCode(action, payload) {
|
|
294
|
+
const serializedAction = JSON.stringify(action);
|
|
295
|
+
const serializedPayload = JSON.stringify(payload || {});
|
|
296
|
+
|
|
297
|
+
return `(() => {
|
|
298
|
+
const action = ${serializedAction};
|
|
299
|
+
const payload = ${serializedPayload};
|
|
300
|
+
const normalize = (value) => typeof value === "string" ? value.trim() : "";
|
|
301
|
+
const byText = (text, exact) => {
|
|
302
|
+
const needle = normalize(text);
|
|
303
|
+
if (!needle) return null;
|
|
304
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
305
|
+
while (walker.nextNode()) {
|
|
306
|
+
const element = walker.currentNode;
|
|
307
|
+
const textContent = normalize(element.textContent || "");
|
|
308
|
+
if (!textContent) continue;
|
|
309
|
+
if ((exact && textContent === needle) || (!exact && textContent.includes(needle))) {
|
|
310
|
+
return element;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
};
|
|
315
|
+
const resolveTarget = () => {
|
|
316
|
+
const aiTag = normalize(payload.ai_tag);
|
|
317
|
+
if (aiTag) {
|
|
318
|
+
return document.querySelector('[data-drive-tag="' + aiTag.replace(/"/g, '\\"') + '"], [ai-tag="' + aiTag.replace(/"/g, '\\"') + '"]');
|
|
319
|
+
}
|
|
320
|
+
const selector = normalize(payload.selector);
|
|
321
|
+
if (selector) {
|
|
322
|
+
return document.querySelector(selector);
|
|
323
|
+
}
|
|
324
|
+
const text = normalize(payload.text);
|
|
325
|
+
if (text) {
|
|
326
|
+
return byText(text, payload.exact === true);
|
|
327
|
+
}
|
|
328
|
+
return document.body;
|
|
329
|
+
};
|
|
330
|
+
const target = resolveTarget();
|
|
331
|
+
if (action !== "screenshot" && action !== "inject_script" && !target) {
|
|
332
|
+
return { ok: false, error: "Target element was not found." };
|
|
333
|
+
}
|
|
334
|
+
if (target && typeof target.scrollIntoView === "function") {
|
|
335
|
+
target.scrollIntoView({ block: "center", inline: "center" });
|
|
336
|
+
}
|
|
337
|
+
const toVisible = (element) => {
|
|
338
|
+
const computed = window.getComputedStyle(element);
|
|
339
|
+
return computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0";
|
|
340
|
+
};
|
|
341
|
+
if (action === "dom") {
|
|
342
|
+
const attributes = target
|
|
343
|
+
? Object.fromEntries(Array.from(target.attributes || []).map((attr) => [attr.name, attr.value]))
|
|
344
|
+
: {};
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
result: {
|
|
348
|
+
found: Boolean(target),
|
|
349
|
+
outer_html: payload.include_html === false ? undefined : target.outerHTML,
|
|
350
|
+
text_content: payload.include_text === false ? undefined : (target.textContent || "").trim(),
|
|
351
|
+
visible: target ? toVisible(target) : false,
|
|
352
|
+
attributes,
|
|
353
|
+
url: location.href,
|
|
354
|
+
title: document.title,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (action === "click") {
|
|
359
|
+
target.click();
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
result: {
|
|
363
|
+
url: location.href,
|
|
364
|
+
title: document.title,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (action === "fill") {
|
|
369
|
+
target.focus();
|
|
370
|
+
target.value = payload.value || "";
|
|
371
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
372
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
result: {
|
|
376
|
+
url: location.href,
|
|
377
|
+
title: document.title,
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (action === "inject_script") {
|
|
382
|
+
const namespace = normalize(payload.namespace) || "TELLY";
|
|
383
|
+
const source = String(payload.source || "");
|
|
384
|
+
if (!source) {
|
|
385
|
+
return { ok: false, error: "Script source is required." };
|
|
386
|
+
}
|
|
387
|
+
const wrapped = "const __tellyNamespace=" + JSON.stringify(namespace)
|
|
388
|
+
+ ";window[__tellyNamespace]=window[__tellyNamespace]||{};var TELLY=window[__tellyNamespace];const __tellyBeforeKeys=new Set(Object.getOwnPropertyNames(window));\\n"
|
|
389
|
+
+ source
|
|
390
|
+
+ "\\nfor(const __tellyKey of Object.getOwnPropertyNames(window)){if(__tellyBeforeKeys.has(__tellyKey)){continue;}if(__tellyKey===__tellyNamespace){continue;}try{window[__tellyNamespace][__tellyKey]=window[__tellyKey];}catch{}}";
|
|
391
|
+
const script = document.createElement("script");
|
|
392
|
+
script.textContent = wrapped;
|
|
393
|
+
(document.documentElement || document.head || document.body).appendChild(script);
|
|
394
|
+
script.remove();
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
result: {
|
|
398
|
+
namespace,
|
|
399
|
+
bytes: source.length,
|
|
400
|
+
url: location.href,
|
|
401
|
+
title: document.title,
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (action === "press") {
|
|
406
|
+
const key = String(payload.key || "");
|
|
407
|
+
if (target && typeof target.focus === "function") {
|
|
408
|
+
target.focus();
|
|
409
|
+
}
|
|
410
|
+
const eventInit = { key, bubbles: true, cancelable: true };
|
|
411
|
+
const keyboardTarget = document.activeElement || target || document.body;
|
|
412
|
+
keyboardTarget.dispatchEvent(new KeyboardEvent("keydown", eventInit));
|
|
413
|
+
keyboardTarget.dispatchEvent(new KeyboardEvent("keypress", eventInit));
|
|
414
|
+
keyboardTarget.dispatchEvent(new KeyboardEvent("keyup", eventInit));
|
|
415
|
+
return {
|
|
416
|
+
ok: true,
|
|
417
|
+
result: {
|
|
418
|
+
url: location.href,
|
|
419
|
+
title: document.title,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return { ok: false, error: "Unsupported tab action." };
|
|
424
|
+
})();`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function runTabAction(tabId, action, payload) {
|
|
428
|
+
const activeTab = await ensureTabIsActive(tabId);
|
|
429
|
+
|
|
430
|
+
if (action === "screenshot") {
|
|
431
|
+
const dataUrl = await captureVisibleTab(activeTab.windowId, {
|
|
432
|
+
format: "png",
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
ok: true,
|
|
436
|
+
result: {
|
|
437
|
+
png_base64: String(dataUrl).replace(/^data:image\/png;base64,/, ""),
|
|
438
|
+
url: activeTab.url || "",
|
|
439
|
+
title: activeTab.title || "",
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (action === "inject_script") {
|
|
445
|
+
const namespace =
|
|
446
|
+
typeof payload?.namespace === "string" && payload.namespace.trim()
|
|
447
|
+
? payload.namespace.trim()
|
|
448
|
+
: "TELLY";
|
|
449
|
+
const source = String(payload?.source || "");
|
|
450
|
+
if (!source) {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
error: "Script source is required.",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const execution = await scriptingExecuteScript({
|
|
458
|
+
target: { tabId },
|
|
459
|
+
world: "MAIN",
|
|
460
|
+
func: runInjectedMainWorldScript,
|
|
461
|
+
args: [namespace, source, collectInjectExportNames(source)],
|
|
462
|
+
});
|
|
463
|
+
const result = execution[0]?.result;
|
|
464
|
+
|
|
465
|
+
if (!result || result.ok !== true) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
error: result?.error || "Tab action did not return a successful result.",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const execution = await scriptingExecuteScript({
|
|
476
|
+
target: { tabId },
|
|
477
|
+
func: new Function(`return ${buildTabActionCode(action, payload)}`),
|
|
478
|
+
});
|
|
479
|
+
const result = execution[0]?.result;
|
|
480
|
+
|
|
481
|
+
if (!result || result.ok !== true) {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: result?.error || "Tab action did not return a successful result.",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function sendJson(payload) {
|
|
492
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
socket.send(JSON.stringify(payload));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function clearTimers() {
|
|
499
|
+
if (reconnectTimer) {
|
|
500
|
+
clearTimeout(reconnectTimer);
|
|
501
|
+
reconnectTimer = null;
|
|
502
|
+
}
|
|
503
|
+
if (heartbeatTimer) {
|
|
504
|
+
clearInterval(heartbeatTimer);
|
|
505
|
+
heartbeatTimer = null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function scheduleReconnect() {
|
|
510
|
+
if (manualDisconnect) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
clearTimers();
|
|
514
|
+
reconnectTimer = setTimeout(() => {
|
|
515
|
+
void connect();
|
|
516
|
+
}, RECONNECT_DELAY_MS);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function waitForSocketReady(timeoutMs = 2500) {
|
|
520
|
+
const deadline = Date.now() + timeoutMs;
|
|
521
|
+
while (Date.now() < deadline) {
|
|
522
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
526
|
+
}
|
|
527
|
+
return Boolean(socket && socket.readyState === WebSocket.OPEN);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function startHeartbeat() {
|
|
531
|
+
if (heartbeatTimer) {
|
|
532
|
+
clearInterval(heartbeatTimer);
|
|
533
|
+
}
|
|
534
|
+
heartbeatTimer = setInterval(() => {
|
|
535
|
+
sendJson({
|
|
536
|
+
type: "heartbeat",
|
|
537
|
+
sent_at: new Date().toISOString(),
|
|
538
|
+
});
|
|
539
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function sendHello() {
|
|
543
|
+
sendJson({
|
|
544
|
+
type: "hello",
|
|
545
|
+
extension_version: browser.runtime.getManifest().version,
|
|
546
|
+
browser: "chrome",
|
|
547
|
+
instance_id: await computeInstanceId(),
|
|
548
|
+
profile_name: "default",
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function bytesToBase64(bytes) {
|
|
553
|
+
let binary = "";
|
|
554
|
+
const chunkSize = 0x8000;
|
|
555
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
556
|
+
const slice = bytes.subarray(index, index + chunkSize);
|
|
557
|
+
binary += String.fromCharCode(...slice);
|
|
558
|
+
}
|
|
559
|
+
return btoa(binary);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function headersToArray(headers) {
|
|
563
|
+
return (headers || []).map((header) => ({
|
|
564
|
+
name: String(header.name || ""),
|
|
565
|
+
value: String(header.value || ""),
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function getRecordingByTabId(tabId) {
|
|
570
|
+
const recordingId = activeRecordingIdByTabId.get(tabId);
|
|
571
|
+
return recordingId ? activeRecordingsById.get(recordingId) || null : null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function sendRecordingEvent(recordingId, tabId, event) {
|
|
575
|
+
if (!recordingId || !Number.isInteger(tabId)) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
sendJson({
|
|
579
|
+
type: "recording_event",
|
|
580
|
+
recording_id: recordingId,
|
|
581
|
+
tab_id: tabId,
|
|
582
|
+
event: {
|
|
583
|
+
...event,
|
|
584
|
+
at: event.at || new Date().toISOString(),
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function injectRecorderContent(tabId) {
|
|
590
|
+
await scriptingExecuteScript({
|
|
591
|
+
target: { tabId },
|
|
592
|
+
files: ["recorder-content.js"],
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function captureTabSnapshot(tabId, reason) {
|
|
597
|
+
const execution = await scriptingExecuteScript({
|
|
598
|
+
target: { tabId },
|
|
599
|
+
func: (recordingReason) => ({
|
|
600
|
+
kind: "page_snapshot",
|
|
601
|
+
source: "background",
|
|
602
|
+
reason: recordingReason,
|
|
603
|
+
at: new Date().toISOString(),
|
|
604
|
+
url: location.href,
|
|
605
|
+
title: document.title,
|
|
606
|
+
ready_state: document.readyState,
|
|
607
|
+
html: document.documentElement ? document.documentElement.outerHTML : ""
|
|
608
|
+
}),
|
|
609
|
+
args: [reason],
|
|
610
|
+
});
|
|
611
|
+
const result = execution[0]?.result;
|
|
612
|
+
return result || null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function emitCookiesSnapshot(recordingId, tabId, tabUrl, tabTitle) {
|
|
616
|
+
if (!tabUrl || !/^https?:\/\//iu.test(tabUrl)) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const cookies = await cookiesGetAll({ url: tabUrl });
|
|
621
|
+
sendRecordingEvent(recordingId, tabId, {
|
|
622
|
+
kind: "cookies_snapshot",
|
|
623
|
+
source: "browser",
|
|
624
|
+
url: tabUrl,
|
|
625
|
+
tab_title: tabTitle || "",
|
|
626
|
+
cookies: cookies.map((cookie) => ({
|
|
627
|
+
name: cookie.name,
|
|
628
|
+
value: cookie.value,
|
|
629
|
+
domain: cookie.domain,
|
|
630
|
+
path: cookie.path,
|
|
631
|
+
secure: cookie.secure,
|
|
632
|
+
http_only: cookie.httpOnly,
|
|
633
|
+
same_site: cookie.sameSite,
|
|
634
|
+
store_id: cookie.storeId,
|
|
635
|
+
})),
|
|
636
|
+
});
|
|
637
|
+
} catch {
|
|
638
|
+
// ignore
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function startRecording(message) {
|
|
643
|
+
const attached = (await storageGet({ [ATTACHED_TAB_KEY]: null }))[ATTACHED_TAB_KEY];
|
|
644
|
+
const tabId = Number(message.tab_id);
|
|
645
|
+
if (!attached || attached.tab_id !== tabId) {
|
|
646
|
+
return {
|
|
647
|
+
ok: false,
|
|
648
|
+
active: false,
|
|
649
|
+
error: "Selected attached tab does not match the requested recording tab.",
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const browserTab = await tabsGet(tabId);
|
|
654
|
+
activeRecordingsById.set(message.recording_id, {
|
|
655
|
+
recordingId: message.recording_id,
|
|
656
|
+
tabId,
|
|
657
|
+
tabTitle: browserTab.title || "",
|
|
658
|
+
tabUrl: browserTab.url || "",
|
|
659
|
+
startedAt: new Date().toISOString(),
|
|
660
|
+
});
|
|
661
|
+
activeRecordingIdByTabId.set(tabId, message.recording_id);
|
|
662
|
+
|
|
663
|
+
await injectRecorderContent(tabId);
|
|
664
|
+
const snapshot = await captureTabSnapshot(tabId, "recording-start");
|
|
665
|
+
if (snapshot) {
|
|
666
|
+
sendRecordingEvent(message.recording_id, tabId, snapshot);
|
|
667
|
+
}
|
|
668
|
+
await emitCookiesSnapshot(
|
|
669
|
+
message.recording_id,
|
|
670
|
+
tabId,
|
|
671
|
+
browserTab.url || "",
|
|
672
|
+
browserTab.title || "",
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
ok: true,
|
|
677
|
+
active: true,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function stopRecording(message) {
|
|
682
|
+
activeRecordingsById.delete(message.recording_id);
|
|
683
|
+
activeRecordingIdByTabId.delete(message.tab_id);
|
|
684
|
+
await setRecordingStatus({
|
|
685
|
+
active: false,
|
|
686
|
+
});
|
|
687
|
+
return {
|
|
688
|
+
ok: true,
|
|
689
|
+
active: false,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function handleAttachTabSelectedCommand(tabId) {
|
|
694
|
+
if (!Number.isInteger(tabId) || tabId < 0) {
|
|
695
|
+
return {
|
|
696
|
+
ok: false,
|
|
697
|
+
error: "Invalid tab_id.",
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
let browserTab;
|
|
702
|
+
try {
|
|
703
|
+
browserTab = await tabsGet(tabId);
|
|
704
|
+
} catch {
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
error: "Tab not found.",
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const ready = await waitForSocketReady();
|
|
712
|
+
if (!ready) {
|
|
713
|
+
return {
|
|
714
|
+
ok: false,
|
|
715
|
+
error: "Extension is not connected to TellyMCP.",
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const record = {
|
|
720
|
+
tab_id: tabId,
|
|
721
|
+
window_id: browserTab.windowId,
|
|
722
|
+
active: browserTab.active === true,
|
|
723
|
+
title: browserTab.title || "",
|
|
724
|
+
url: browserTab.url || "",
|
|
725
|
+
status: browserTab.status || "",
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
sendJson({
|
|
729
|
+
type: "attach_tab_selected",
|
|
730
|
+
tab: record,
|
|
731
|
+
});
|
|
732
|
+
await setAttachedTab(record);
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
ok: true,
|
|
736
|
+
tab: record,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function handlePopupCommand(command) {
|
|
741
|
+
if (!command || typeof command !== "object") {
|
|
742
|
+
return {
|
|
743
|
+
ok: false,
|
|
744
|
+
error: "Invalid popup command.",
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (command.type === "attach_tab_selected") {
|
|
749
|
+
return await handleAttachTabSelectedCommand(Number(command.tab_id));
|
|
750
|
+
}
|
|
751
|
+
if (command.type === "attach_recording_start") {
|
|
752
|
+
const stored = await storageGet({ [ATTACHED_TAB_KEY]: null });
|
|
753
|
+
const tab = stored[ATTACHED_TAB_KEY];
|
|
754
|
+
if (!tab) {
|
|
755
|
+
return {
|
|
756
|
+
ok: false,
|
|
757
|
+
active: false,
|
|
758
|
+
error: "Select a tab first.",
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
return await sendManualRecordingRequest("recording_manual_start", { tab });
|
|
762
|
+
}
|
|
763
|
+
if (command.type === "attach_recording_stop") {
|
|
764
|
+
return await sendManualRecordingRequest("recording_manual_stop");
|
|
765
|
+
}
|
|
766
|
+
if (command.type === "attach_recording_status") {
|
|
767
|
+
return await sendManualRecordingRequest("recording_manual_status");
|
|
768
|
+
}
|
|
769
|
+
if (command.type === "attach_inject_script") {
|
|
770
|
+
const stored = await storageGet({ [ATTACHED_TAB_KEY]: null });
|
|
771
|
+
const tab = stored[ATTACHED_TAB_KEY];
|
|
772
|
+
if (!tab) {
|
|
773
|
+
return {
|
|
774
|
+
ok: false,
|
|
775
|
+
error: "Select a tab first.",
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const source = String(command.source || "");
|
|
779
|
+
if (!source.trim()) {
|
|
780
|
+
return {
|
|
781
|
+
ok: false,
|
|
782
|
+
error: "Script source is empty.",
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return await runTabAction(tab.tab_id, "inject_script", {
|
|
786
|
+
namespace: typeof command.namespace === "string" ? command.namespace : "TELLY",
|
|
787
|
+
source,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
error: `Unsupported popup command: ${String(command.type || "")}`,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function sendManualRecordingRequest(type, payload = {}) {
|
|
798
|
+
const ready = await waitForSocketReady();
|
|
799
|
+
if (!ready) {
|
|
800
|
+
return {
|
|
801
|
+
ok: false,
|
|
802
|
+
active: false,
|
|
803
|
+
error: "Extension is not connected to TellyMCP.",
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const requestId = `manual-recording-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
808
|
+
const result = await new Promise((resolve, reject) => {
|
|
809
|
+
const timer = setTimeout(() => {
|
|
810
|
+
pendingManualRecordingRequests.delete(requestId);
|
|
811
|
+
reject(new Error("Recording request timed out."));
|
|
812
|
+
}, 15000);
|
|
813
|
+
pendingManualRecordingRequests.set(requestId, { resolve, reject, timer });
|
|
814
|
+
sendJson({
|
|
815
|
+
type,
|
|
816
|
+
request_id: requestId,
|
|
817
|
+
...payload,
|
|
818
|
+
});
|
|
819
|
+
}).catch((error) => ({
|
|
820
|
+
ok: false,
|
|
821
|
+
active: false,
|
|
822
|
+
error: error instanceof Error ? error.message : String(error),
|
|
823
|
+
}));
|
|
824
|
+
|
|
825
|
+
if (result && typeof result === "object" && result.recording) {
|
|
826
|
+
await setRecordingStatus(result);
|
|
827
|
+
} else if (result && typeof result === "object" && result.active === false) {
|
|
828
|
+
await setRecordingStatus({ active: false });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function handleMessage(rawData) {
|
|
835
|
+
const message =
|
|
836
|
+
typeof rawData === "string"
|
|
837
|
+
? JSON.parse(rawData)
|
|
838
|
+
: JSON.parse(String(rawData));
|
|
839
|
+
|
|
840
|
+
switch (message.type) {
|
|
841
|
+
case "hello_ack":
|
|
842
|
+
await setConnectionStatus({
|
|
843
|
+
state: "connected",
|
|
844
|
+
text: `Connected: ${message.session_label || message.session_id || message.instance_id}`,
|
|
845
|
+
instance_id: message.instance_id,
|
|
846
|
+
...(message.session_id ? { session_id: message.session_id } : {}),
|
|
847
|
+
...(message.session_label ? { session_label: message.session_label } : {}),
|
|
848
|
+
});
|
|
849
|
+
startHeartbeat();
|
|
850
|
+
return;
|
|
851
|
+
case "list_tabs": {
|
|
852
|
+
sendJson({
|
|
853
|
+
type: "list_tabs_result",
|
|
854
|
+
request_id: message.request_id,
|
|
855
|
+
tabs: await listTabs(),
|
|
856
|
+
});
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
case "get_active_tab": {
|
|
860
|
+
sendJson({
|
|
861
|
+
type: "get_active_tab_result",
|
|
862
|
+
request_id: message.request_id,
|
|
863
|
+
tab: await getActiveTab(),
|
|
864
|
+
});
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
case "tab_action": {
|
|
868
|
+
const result = await runTabAction(
|
|
869
|
+
message.tab_id,
|
|
870
|
+
message.action,
|
|
871
|
+
message.payload || {},
|
|
872
|
+
);
|
|
873
|
+
sendJson({
|
|
874
|
+
type: "tab_action_result",
|
|
875
|
+
request_id: message.request_id,
|
|
876
|
+
ok: result.ok === true,
|
|
877
|
+
...(result.result ? { result: result.result } : {}),
|
|
878
|
+
...(result.error ? { error: result.error } : {}),
|
|
879
|
+
});
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
case "recording_start": {
|
|
883
|
+
const result = await startRecording(message);
|
|
884
|
+
sendJson({
|
|
885
|
+
type: "recording_control_result",
|
|
886
|
+
request_id: message.request_id,
|
|
887
|
+
ok: result.ok === true,
|
|
888
|
+
active: result.active === true,
|
|
889
|
+
...(result.error ? { error: result.error } : {}),
|
|
890
|
+
});
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
case "recording_stop": {
|
|
894
|
+
const result = await stopRecording(message);
|
|
895
|
+
sendJson({
|
|
896
|
+
type: "recording_control_result",
|
|
897
|
+
request_id: message.request_id,
|
|
898
|
+
ok: result.ok === true,
|
|
899
|
+
active: result.active === true,
|
|
900
|
+
...(result.error ? { error: result.error } : {}),
|
|
901
|
+
});
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
case "recording_manual_result": {
|
|
905
|
+
const pending = pendingManualRecordingRequests.get(message.request_id);
|
|
906
|
+
if (!pending) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
clearTimeout(pending.timer);
|
|
910
|
+
pendingManualRecordingRequests.delete(message.request_id);
|
|
911
|
+
pending.resolve(message);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
case "recording_state":
|
|
915
|
+
await setRecordingStatus({
|
|
916
|
+
active: message.active === true,
|
|
917
|
+
...(message.recording ? { recording: message.recording } : {}),
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
default:
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function connect() {
|
|
926
|
+
const settings = await getSettings();
|
|
927
|
+
manualDisconnect = settings[CONNECTION_ENABLED_KEY] === false;
|
|
928
|
+
if (manualDisconnect) {
|
|
929
|
+
clearTimers();
|
|
930
|
+
await setConnectionStatus({
|
|
931
|
+
state: "disconnected",
|
|
932
|
+
text: "Disconnected: manual",
|
|
933
|
+
});
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
clearTimers();
|
|
937
|
+
await setConnectionStatus({
|
|
938
|
+
state: "connecting",
|
|
939
|
+
text: `Connecting: ${settings.host}:${settings.port}`,
|
|
940
|
+
});
|
|
941
|
+
const wsUrl = buildWebSocketUrl(settings);
|
|
942
|
+
socket = new WebSocket(wsUrl);
|
|
943
|
+
|
|
944
|
+
socket.addEventListener("open", () => {
|
|
945
|
+
void sendHello();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
socket.addEventListener("message", (event) => {
|
|
949
|
+
void handleMessage(event.data);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
socket.addEventListener("close", () => {
|
|
953
|
+
socket = null;
|
|
954
|
+
if (manualDisconnect) {
|
|
955
|
+
void setConnectionStatus({
|
|
956
|
+
state: "disconnected",
|
|
957
|
+
text: "Disconnected: manual",
|
|
958
|
+
});
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
void setConnectionStatus({
|
|
962
|
+
state: "disconnected",
|
|
963
|
+
text: `Disconnected: reconnecting in ${Math.floor(RECONNECT_DELAY_MS / 1000)}s`,
|
|
964
|
+
});
|
|
965
|
+
scheduleReconnect();
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
socket.addEventListener("error", () => {
|
|
969
|
+
void setConnectionStatus({
|
|
970
|
+
state: "disconnected",
|
|
971
|
+
text: "Disconnected: WebSocket error",
|
|
972
|
+
});
|
|
973
|
+
if (socket) {
|
|
974
|
+
socket.close();
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
browser.storage.onChanged.addListener((changes, areaName) => {
|
|
980
|
+
if (areaName !== "local") {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (changes[CONNECTION_ENABLED_KEY] !== undefined) {
|
|
985
|
+
const nextEnabled = changes[CONNECTION_ENABLED_KEY].newValue !== false;
|
|
986
|
+
manualDisconnect = !nextEnabled;
|
|
987
|
+
if (nextEnabled) {
|
|
988
|
+
if (socket) {
|
|
989
|
+
socket.close();
|
|
990
|
+
} else {
|
|
991
|
+
void connect();
|
|
992
|
+
}
|
|
993
|
+
} else {
|
|
994
|
+
clearTimers();
|
|
995
|
+
if (socket) {
|
|
996
|
+
socket.close();
|
|
997
|
+
} else {
|
|
998
|
+
void setConnectionStatus({
|
|
999
|
+
state: "disconnected",
|
|
1000
|
+
text: "Disconnected: manual",
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (changes[POPUP_COMMAND_KEY] !== undefined) {
|
|
1008
|
+
const command = changes[POPUP_COMMAND_KEY].newValue;
|
|
1009
|
+
if (!command) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
void handlePopupCommand(command)
|
|
1013
|
+
.then((result) =>
|
|
1014
|
+
storageSet({
|
|
1015
|
+
[POPUP_COMMAND_RESULT_KEY]: {
|
|
1016
|
+
command_id: command.command_id,
|
|
1017
|
+
result,
|
|
1018
|
+
at: new Date().toISOString(),
|
|
1019
|
+
},
|
|
1020
|
+
}),
|
|
1021
|
+
)
|
|
1022
|
+
.catch((error) =>
|
|
1023
|
+
storageSet({
|
|
1024
|
+
[POPUP_COMMAND_RESULT_KEY]: {
|
|
1025
|
+
command_id: command.command_id,
|
|
1026
|
+
result: {
|
|
1027
|
+
ok: false,
|
|
1028
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1029
|
+
},
|
|
1030
|
+
at: new Date().toISOString(),
|
|
1031
|
+
},
|
|
1032
|
+
}),
|
|
1033
|
+
);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (
|
|
1038
|
+
changes.host === undefined &&
|
|
1039
|
+
changes.port === undefined
|
|
1040
|
+
) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (socket) {
|
|
1045
|
+
socket.close();
|
|
1046
|
+
} else if (!manualDisconnect) {
|
|
1047
|
+
void connect();
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
browser.runtime.onConnect.addListener((port) => {
|
|
1052
|
+
if (port.name !== CONTROL_PANEL_PORT_NAME) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
controlPanelPorts.add(port);
|
|
1056
|
+
port.onDisconnect.addListener(() => {
|
|
1057
|
+
controlPanelPorts.delete(port);
|
|
1058
|
+
});
|
|
1059
|
+
port.onMessage.addListener(() => {
|
|
1060
|
+
// keep the service worker alive while the control panel is open
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
browser.tabs.onActivated.addListener(async () => {
|
|
1065
|
+
const tab = await getActiveTab();
|
|
1066
|
+
if (!tab) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
sendJson({
|
|
1070
|
+
type: "active_tab_changed",
|
|
1071
|
+
tab,
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|
1076
|
+
if (
|
|
1077
|
+
typeof tabId !== "number" ||
|
|
1078
|
+
(changeInfo.title === undefined &&
|
|
1079
|
+
changeInfo.url === undefined &&
|
|
1080
|
+
changeInfo.status === undefined)
|
|
1081
|
+
) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
sendJson({
|
|
1086
|
+
type: "tab_updated",
|
|
1087
|
+
tab: {
|
|
1088
|
+
tab_id: tabId,
|
|
1089
|
+
window_id: tab.windowId,
|
|
1090
|
+
active: tab.active === true,
|
|
1091
|
+
title: tab.title || "",
|
|
1092
|
+
url: tab.url || "",
|
|
1093
|
+
status: changeInfo.status || tab.status || "",
|
|
1094
|
+
},
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const recording = getRecordingByTabId(tabId);
|
|
1098
|
+
if (!recording) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
sendRecordingEvent(recording.recordingId, tabId, {
|
|
1103
|
+
kind: "navigation",
|
|
1104
|
+
source: "browser",
|
|
1105
|
+
status: changeInfo.status || tab.status || "",
|
|
1106
|
+
url: tab.url || "",
|
|
1107
|
+
title: tab.title || "",
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
if (changeInfo.status === "complete") {
|
|
1111
|
+
try {
|
|
1112
|
+
await injectRecorderContent(tabId);
|
|
1113
|
+
const snapshot = await captureTabSnapshot(tabId, "tab-updated-complete");
|
|
1114
|
+
if (snapshot) {
|
|
1115
|
+
sendRecordingEvent(recording.recordingId, tabId, snapshot);
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
// ignore
|
|
1119
|
+
}
|
|
1120
|
+
await emitCookiesSnapshot(
|
|
1121
|
+
recording.recordingId,
|
|
1122
|
+
tabId,
|
|
1123
|
+
tab.url || "",
|
|
1124
|
+
tab.title || "",
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
async function handleRuntimeMessage(message, sender) {
|
|
1130
|
+
if (!message || typeof message !== "object") {
|
|
1131
|
+
return undefined;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (message.type === "attach_tab_selected") {
|
|
1135
|
+
return await handleAttachTabSelectedCommand(Number(message.tab_id));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (message.type === "attach_connection_set_enabled") {
|
|
1139
|
+
return (async () => {
|
|
1140
|
+
const enabled = message.enabled === true;
|
|
1141
|
+
await storageSet({
|
|
1142
|
+
[CONNECTION_ENABLED_KEY]: enabled,
|
|
1143
|
+
});
|
|
1144
|
+
return { ok: true, enabled };
|
|
1145
|
+
})();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (message.type === "attach_recording_start") {
|
|
1149
|
+
return await handlePopupCommand({ type: "attach_recording_start" });
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (message.type === "attach_recording_stop") {
|
|
1153
|
+
return await handlePopupCommand({ type: "attach_recording_stop" });
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (message.type === "attach_recording_status") {
|
|
1157
|
+
return await handlePopupCommand({ type: "attach_recording_status" });
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (message.type === "attach_inject_script") {
|
|
1161
|
+
return await handlePopupCommand({
|
|
1162
|
+
type: "attach_inject_script",
|
|
1163
|
+
source: message.source,
|
|
1164
|
+
namespace: message.namespace,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (message.type === "telly_recording_page_event") {
|
|
1169
|
+
const tabId = sender?.tab?.id;
|
|
1170
|
+
if (!Number.isInteger(tabId)) {
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
const recording = getRecordingByTabId(tabId);
|
|
1174
|
+
if (!recording) {
|
|
1175
|
+
return undefined;
|
|
1176
|
+
}
|
|
1177
|
+
sendRecordingEvent(recording.recordingId, tabId, message.event || {});
|
|
1178
|
+
return { ok: true };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1185
|
+
void handleRuntimeMessage(message, sender)
|
|
1186
|
+
.then((result) => {
|
|
1187
|
+
sendResponse(result);
|
|
1188
|
+
})
|
|
1189
|
+
.catch((error) => {
|
|
1190
|
+
sendResponse({
|
|
1191
|
+
ok: false,
|
|
1192
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1193
|
+
});
|
|
1194
|
+
});
|
|
1195
|
+
return true;
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
browser.webRequest.onBeforeRequest.addListener(
|
|
1199
|
+
(details) => {
|
|
1200
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1201
|
+
if (!recording) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
let bodyText = "";
|
|
1206
|
+
let bodyBase64 = "";
|
|
1207
|
+
if (details.requestBody?.formData) {
|
|
1208
|
+
bodyText = Object.entries(details.requestBody.formData)
|
|
1209
|
+
.map(([name, values]) =>
|
|
1210
|
+
values.map((value) => `${name}=${String(value)}`).join("&"),
|
|
1211
|
+
)
|
|
1212
|
+
.join("&");
|
|
1213
|
+
} else if (Array.isArray(details.requestBody?.raw)) {
|
|
1214
|
+
const chunks = details.requestBody.raw
|
|
1215
|
+
.map((item) => item.bytes)
|
|
1216
|
+
.filter(Boolean)
|
|
1217
|
+
.map((buffer) => new Uint8Array(buffer));
|
|
1218
|
+
if (chunks.length > 0) {
|
|
1219
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1220
|
+
const merged = new Uint8Array(total);
|
|
1221
|
+
let offset = 0;
|
|
1222
|
+
for (const chunk of chunks) {
|
|
1223
|
+
merged.set(chunk, offset);
|
|
1224
|
+
offset += chunk.length;
|
|
1225
|
+
}
|
|
1226
|
+
bodyBase64 = bytesToBase64(merged.subarray(0, MAX_CAPTURE_BYTES));
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1231
|
+
kind: "network_request",
|
|
1232
|
+
source: "browser",
|
|
1233
|
+
request_id: details.requestId,
|
|
1234
|
+
url: details.url,
|
|
1235
|
+
method: details.method,
|
|
1236
|
+
resource_type: details.type,
|
|
1237
|
+
...(bodyText ? { body_text: bodyText.slice(0, MAX_CAPTURE_BYTES) } : {}),
|
|
1238
|
+
...(bodyBase64 ? { body_base64: bodyBase64 } : {}),
|
|
1239
|
+
});
|
|
1240
|
+
},
|
|
1241
|
+
{ urls: ["<all_urls>"] },
|
|
1242
|
+
["requestBody"],
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
browser.webRequest.onBeforeSendHeaders.addListener(
|
|
1246
|
+
(details) => {
|
|
1247
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1248
|
+
if (!recording) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1252
|
+
kind: "network_request_headers",
|
|
1253
|
+
source: "browser",
|
|
1254
|
+
request_id: details.requestId,
|
|
1255
|
+
url: details.url,
|
|
1256
|
+
method: details.method,
|
|
1257
|
+
resource_type: details.type,
|
|
1258
|
+
headers: headersToArray(details.requestHeaders),
|
|
1259
|
+
});
|
|
1260
|
+
},
|
|
1261
|
+
{ urls: ["<all_urls>"] },
|
|
1262
|
+
["requestHeaders"],
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
browser.webRequest.onHeadersReceived.addListener(
|
|
1266
|
+
(details) => {
|
|
1267
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1268
|
+
if (!recording) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1273
|
+
kind: "network_response_headers",
|
|
1274
|
+
source: "browser",
|
|
1275
|
+
request_id: details.requestId,
|
|
1276
|
+
url: details.url,
|
|
1277
|
+
method: details.method,
|
|
1278
|
+
resource_type: details.type,
|
|
1279
|
+
status_code: details.statusCode,
|
|
1280
|
+
headers: headersToArray(details.responseHeaders),
|
|
1281
|
+
});
|
|
1282
|
+
},
|
|
1283
|
+
{ urls: ["<all_urls>"] },
|
|
1284
|
+
["responseHeaders"],
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
browser.webRequest.onCompleted.addListener(
|
|
1288
|
+
(details) => {
|
|
1289
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1290
|
+
if (!recording) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1294
|
+
kind: "network_response_complete",
|
|
1295
|
+
source: "browser",
|
|
1296
|
+
request_id: details.requestId,
|
|
1297
|
+
url: details.url,
|
|
1298
|
+
method: details.method,
|
|
1299
|
+
resource_type: details.type,
|
|
1300
|
+
status_code: details.statusCode,
|
|
1301
|
+
});
|
|
1302
|
+
},
|
|
1303
|
+
{ urls: ["<all_urls>"] },
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
browser.webRequest.onErrorOccurred.addListener(
|
|
1307
|
+
(details) => {
|
|
1308
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1309
|
+
if (!recording) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1313
|
+
kind: "network_error",
|
|
1314
|
+
source: "browser",
|
|
1315
|
+
request_id: details.requestId,
|
|
1316
|
+
url: details.url,
|
|
1317
|
+
method: details.method,
|
|
1318
|
+
resource_type: details.type,
|
|
1319
|
+
error: details.error,
|
|
1320
|
+
});
|
|
1321
|
+
},
|
|
1322
|
+
{ urls: ["<all_urls>"] },
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1325
|
+
void computeInstanceId();
|
|
1326
|
+
void connect();
|