@deadragdoll/tellymcp 0.0.12 → 0.0.14
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/.env.example.client +5 -1
- package/.env.example.gateway +16 -1
- package/CHANGELOG.md +30 -0
- package/README-ru.md +33 -0
- package/README.md +33 -0
- package/TOOLS.md +384 -9
- package/config/templates/env.both.template +15 -2
- package/config/templates/env.client.template +5 -2
- package/config/templates/env.gateway.template +15 -2
- package/dist/cli.js +137 -2
- package/dist/services/features/telegram-mcp/browser.service.js +56 -1
- package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
- package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
- package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +30 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +23 -1
- package/dist/services/features/telegram-mcp/src/app/config/env.js +132 -2
- package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
- package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
- package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +205 -17
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
- 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 +890 -23
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +536 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +80 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +736 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +188 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
- package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
- package/docs/CHAT_CONNECTOR.md +134 -0
- package/docs/STANDALONE-ru.md +55 -6
- package/docs/STANDALONE.md +55 -6
- package/package.json +10 -5
- package/packages/chrome-attach-extension/dist/background.js +1735 -0
- package/packages/chrome-attach-extension/dist/icon.svg +6 -0
- package/packages/chrome-attach-extension/dist/manifest.json +37 -0
- package/packages/chrome-attach-extension/dist/options.html +312 -0
- package/packages/chrome-attach-extension/dist/options.js +606 -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 +96 -0
- package/packages/chrome-attach-extension/dist/recorder-page.js +282 -0
- package/packages/firefox-attach-extension/README.md +13 -0
- package/packages/firefox-attach-extension/dist/background.js +1622 -0
- package/packages/firefox-attach-extension/dist/icon.svg +6 -0
- package/packages/firefox-attach-extension/dist/manifest.json +44 -0
- package/packages/firefox-attach-extension/dist/options.html +312 -0
- package/packages/firefox-attach-extension/dist/options.js +540 -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 +90 -0
- package/packages/firefox-attach-extension/dist/recorder-page.js +318 -0
|
@@ -0,0 +1,1735 @@
|
|
|
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 KEEPALIVE_ALARM_NAME = "telly-keepalive";
|
|
19
|
+
const KEEPALIVE_ALARM_PERIOD_MINUTES = 0.5;
|
|
20
|
+
const MAX_CAPTURE_BYTES = 512 * 1024;
|
|
21
|
+
const MAX_BUFFERED_LOG_ENTRIES = 200;
|
|
22
|
+
|
|
23
|
+
let socket = null;
|
|
24
|
+
let reconnectTimer = null;
|
|
25
|
+
let heartbeatTimer = null;
|
|
26
|
+
let instanceId = null;
|
|
27
|
+
let attachedTabId = null;
|
|
28
|
+
let manualDisconnect = false;
|
|
29
|
+
const pendingManualRecordingRequests = new Map();
|
|
30
|
+
const controlPanelPorts = new Set();
|
|
31
|
+
|
|
32
|
+
const activeRecordingsById = new Map();
|
|
33
|
+
const activeRecordingIdByTabId = new Map();
|
|
34
|
+
const consoleMessagesByTabId = new Map();
|
|
35
|
+
const pageErrorsByTabId = new Map();
|
|
36
|
+
const networkFailuresByTabId = new Map();
|
|
37
|
+
|
|
38
|
+
function padNumber(value, length = 2) {
|
|
39
|
+
return String(value).padStart(length, "0");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatLocalTimestamp(date = new Date()) {
|
|
43
|
+
return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(
|
|
44
|
+
date.getHours(),
|
|
45
|
+
)}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(
|
|
46
|
+
date.getMilliseconds(),
|
|
47
|
+
3,
|
|
48
|
+
)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function storageGet(defaults) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
browser.storage.local.get(defaults, (result) => {
|
|
54
|
+
const error = browser.runtime?.lastError;
|
|
55
|
+
if (error) {
|
|
56
|
+
reject(new Error(error.message));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
resolve(result);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function storageSet(value) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
browser.storage.local.set(value, () => {
|
|
67
|
+
const error = browser.runtime?.lastError;
|
|
68
|
+
if (error) {
|
|
69
|
+
reject(new Error(error.message));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tabsQuery(queryInfo) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
browser.tabs.query(queryInfo, (tabs) => {
|
|
80
|
+
const error = browser.runtime?.lastError;
|
|
81
|
+
if (error) {
|
|
82
|
+
reject(new Error(error.message));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
resolve(tabs || []);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function tabsGet(tabId) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
browser.tabs.get(tabId, (tab) => {
|
|
93
|
+
const error = browser.runtime?.lastError;
|
|
94
|
+
if (error) {
|
|
95
|
+
reject(new Error(error.message));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolve(tab);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function tabsUpdate(tabId, updateProperties) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
browser.tabs.update(tabId, updateProperties, (tab) => {
|
|
106
|
+
const error = browser.runtime?.lastError;
|
|
107
|
+
if (error) {
|
|
108
|
+
reject(new Error(error.message));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
resolve(tab);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function windowsUpdate(windowId, updateInfo) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
browser.windows.update(windowId, updateInfo, (window) => {
|
|
119
|
+
const error = browser.runtime?.lastError;
|
|
120
|
+
if (error) {
|
|
121
|
+
reject(new Error(error.message));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
resolve(window);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function tabsRemove(tabId) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
browser.tabs.remove(tabId, () => {
|
|
132
|
+
const error = browser.runtime?.lastError;
|
|
133
|
+
if (error) {
|
|
134
|
+
reject(new Error(error.message));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
resolve();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cookiesGetAll(details) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
browser.cookies.getAll(details, (cookies) => {
|
|
145
|
+
const error = browser.runtime?.lastError;
|
|
146
|
+
if (error) {
|
|
147
|
+
reject(new Error(error.message));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
resolve(cookies || []);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function captureVisibleTab(windowId, options) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
browser.tabs.captureVisibleTab(windowId, options, (dataUrl) => {
|
|
158
|
+
const error = browser.runtime?.lastError;
|
|
159
|
+
if (error) {
|
|
160
|
+
reject(new Error(error.message));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
resolve(dataUrl);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function scriptingExecuteScript(options) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
browser.scripting.executeScript(options, (result) => {
|
|
171
|
+
const error = browser.runtime?.lastError;
|
|
172
|
+
if (error) {
|
|
173
|
+
reject(new Error(error.message));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
resolve(result || []);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function collectInjectExportNames(source) {
|
|
182
|
+
const names = new Set();
|
|
183
|
+
const patterns = [
|
|
184
|
+
/\basync\s+function\s+([A-Za-z_$][\w$]*)\s*\(/gu,
|
|
185
|
+
/\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/gu,
|
|
186
|
+
/\bclass\s+([A-Za-z_$][\w$]*)\s*(?:\{|extends\b)/gu,
|
|
187
|
+
/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=/gu,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
for (const pattern of patterns) {
|
|
191
|
+
for (const match of source.matchAll(pattern)) {
|
|
192
|
+
const name = match[1];
|
|
193
|
+
if (name && name !== "TELLY") {
|
|
194
|
+
names.add(name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return [...names];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function runInjectedMainWorldScript(namespace, source, exportNames) {
|
|
203
|
+
try {
|
|
204
|
+
window[namespace] = window[namespace] || {};
|
|
205
|
+
const wrapped = "window[" + JSON.stringify(namespace) + "]=window[" + JSON.stringify(namespace) + "]||{};var TELLY=window[" + JSON.stringify(namespace) + "];\n"
|
|
206
|
+
+ String(source || "");
|
|
207
|
+
|
|
208
|
+
const script = document.createElement("script");
|
|
209
|
+
script.textContent = wrapped;
|
|
210
|
+
(document.documentElement || document.head || document.body).appendChild(script);
|
|
211
|
+
script.remove();
|
|
212
|
+
|
|
213
|
+
for (const name of Array.isArray(exportNames) ? exportNames : []) {
|
|
214
|
+
if (!name || !(name in window)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
window[namespace][name] = window[name];
|
|
219
|
+
} catch {
|
|
220
|
+
// ignore unassignable globals
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
result: {
|
|
227
|
+
namespace,
|
|
228
|
+
bytes: String(source || "").length,
|
|
229
|
+
url: location.href,
|
|
230
|
+
title: document.title,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
error: error instanceof Error ? error.message : String(error),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function getSettings() {
|
|
242
|
+
return await storageGet({
|
|
243
|
+
...DEFAULT_SETTINGS,
|
|
244
|
+
[CONNECTION_ENABLED_KEY]: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function setConnectionStatus(status) {
|
|
249
|
+
await storageSet({
|
|
250
|
+
[CONNECTION_STATUS_KEY]: status,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function setLocalInstanceId(value) {
|
|
255
|
+
await storageSet({
|
|
256
|
+
[INSTANCE_ID_KEY]: value,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function setAttachedTab(tab) {
|
|
261
|
+
attachedTabId =
|
|
262
|
+
tab && Number.isInteger(tab.tab_id)
|
|
263
|
+
? Number(tab.tab_id)
|
|
264
|
+
: null;
|
|
265
|
+
await storageSet({
|
|
266
|
+
[ATTACHED_TAB_KEY]: tab,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function setRecordingStatus(status) {
|
|
271
|
+
await storageSet({
|
|
272
|
+
[RECORDING_STATUS_KEY]: status,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function hydrateAttachedTabSelection() {
|
|
277
|
+
const stored = await storageGet({ [ATTACHED_TAB_KEY]: null });
|
|
278
|
+
const tab = stored[ATTACHED_TAB_KEY];
|
|
279
|
+
attachedTabId =
|
|
280
|
+
tab && Number.isInteger(tab.tab_id)
|
|
281
|
+
? Number(tab.tab_id)
|
|
282
|
+
: null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildWebSocketUrl(settings) {
|
|
286
|
+
return `ws://${settings.host}:${settings.port}/browser-attach/ws`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function computeInstanceId() {
|
|
290
|
+
if (instanceId) {
|
|
291
|
+
return instanceId;
|
|
292
|
+
}
|
|
293
|
+
const runtimeId = browser.runtime.id || "chrome";
|
|
294
|
+
instanceId = `chrome-${runtimeId}`;
|
|
295
|
+
await setLocalInstanceId(instanceId);
|
|
296
|
+
return instanceId;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function listTabs() {
|
|
300
|
+
const tabs = await tabsQuery({});
|
|
301
|
+
return tabs.map((tab) => ({
|
|
302
|
+
tab_id: tab.id,
|
|
303
|
+
window_id: tab.windowId,
|
|
304
|
+
active: tab.active === true,
|
|
305
|
+
title: tab.title || "",
|
|
306
|
+
url: tab.url || "",
|
|
307
|
+
status: tab.status || "",
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function getActiveTab() {
|
|
312
|
+
const tabs = await tabsQuery({ active: true, currentWindow: true });
|
|
313
|
+
const tab = tabs[0];
|
|
314
|
+
if (!tab || typeof tab.id !== "number") {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
tab_id: tab.id,
|
|
319
|
+
window_id: tab.windowId,
|
|
320
|
+
active: tab.active === true,
|
|
321
|
+
title: tab.title || "",
|
|
322
|
+
url: tab.url || "",
|
|
323
|
+
status: tab.status || "",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function ensureTabIsActive(tabId) {
|
|
328
|
+
let tab;
|
|
329
|
+
try {
|
|
330
|
+
tab = await tabsGet(tabId);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error("Tab not found.");
|
|
333
|
+
}
|
|
334
|
+
await tabsUpdate(tabId, { active: true });
|
|
335
|
+
await windowsUpdate(tab.windowId, { focused: true });
|
|
336
|
+
return await tabsGet(tabId);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function trimBufferedEntries(entries) {
|
|
340
|
+
if (entries.length <= MAX_BUFFERED_LOG_ENTRIES) {
|
|
341
|
+
return entries;
|
|
342
|
+
}
|
|
343
|
+
return entries.slice(entries.length - MAX_BUFFERED_LOG_ENTRIES);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function appendBufferedEntry(store, tabId, entry) {
|
|
347
|
+
if (!Number.isInteger(tabId) || tabId < 0 || !entry || typeof entry !== "object") {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const existing = store.get(tabId) || [];
|
|
351
|
+
existing.push(entry);
|
|
352
|
+
store.set(tabId, trimBufferedEntries(existing));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function extractErrorStack(args) {
|
|
356
|
+
if (!Array.isArray(args)) {
|
|
357
|
+
return "";
|
|
358
|
+
}
|
|
359
|
+
for (const arg of args) {
|
|
360
|
+
if (!arg || typeof arg !== "object") {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (typeof arg.stack === "string" && arg.stack.trim()) {
|
|
364
|
+
return arg.stack;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return "";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function extractConsoleLocation(event) {
|
|
371
|
+
if (!event || typeof event !== "object" || !Array.isArray(event.args)) {
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
for (const arg of event.args) {
|
|
375
|
+
if (!arg || typeof arg !== "object") {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (typeof arg.filename === "string" && arg.filename.trim()) {
|
|
379
|
+
const line = Number.isFinite(arg.lineno) ? Number(arg.lineno) : 0;
|
|
380
|
+
const column = Number.isFinite(arg.colno) ? Number(arg.colno) : 0;
|
|
381
|
+
return `${arg.filename}:${line}:${column}`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return "";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function recordPageEventForBuffers(tabId, event) {
|
|
388
|
+
if (!Number.isInteger(tabId) || tabId < 0 || !event || typeof event !== "object") {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (event.kind !== "console_event") {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const timestamp =
|
|
396
|
+
typeof event.at === "string" && event.at.trim()
|
|
397
|
+
? event.at
|
|
398
|
+
: formatLocalTimestamp(new Date());
|
|
399
|
+
const text = typeof event.text === "string" ? event.text : "";
|
|
400
|
+
const location = extractConsoleLocation(event);
|
|
401
|
+
|
|
402
|
+
appendBufferedEntry(consoleMessagesByTabId, tabId, {
|
|
403
|
+
type:
|
|
404
|
+
typeof event.level === "string" && event.level.trim()
|
|
405
|
+
? event.level
|
|
406
|
+
: "log",
|
|
407
|
+
text,
|
|
408
|
+
...(location ? { location } : {}),
|
|
409
|
+
timestamp,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (event.level === "error" || event.level === "assert") {
|
|
413
|
+
const stack = extractErrorStack(event.args);
|
|
414
|
+
appendBufferedEntry(pageErrorsByTabId, tabId, {
|
|
415
|
+
message: text || "Console error",
|
|
416
|
+
...(stack ? { stack } : {}),
|
|
417
|
+
timestamp,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function recordNetworkFailure(tabId, event) {
|
|
423
|
+
if (!Number.isInteger(tabId) || tabId < 0 || !event || typeof event !== "object") {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
appendBufferedEntry(networkFailuresByTabId, tabId, {
|
|
427
|
+
url: typeof event.url === "string" ? event.url : "",
|
|
428
|
+
method:
|
|
429
|
+
typeof event.method === "string" && event.method.trim()
|
|
430
|
+
? event.method
|
|
431
|
+
: "GET",
|
|
432
|
+
...(typeof event.status === "number" ? { status: event.status } : {}),
|
|
433
|
+
...(typeof event.error_text === "string" ? { error_text: event.error_text } : {}),
|
|
434
|
+
...(typeof event.resource_type === "string"
|
|
435
|
+
? { resource_type: event.resource_type }
|
|
436
|
+
: {}),
|
|
437
|
+
timestamp:
|
|
438
|
+
typeof event.timestamp === "string" && event.timestamp.trim()
|
|
439
|
+
? event.timestamp
|
|
440
|
+
: formatLocalTimestamp(new Date()),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function getBufferedLogSnapshot(tabId, limit) {
|
|
445
|
+
const normalize = (value) =>
|
|
446
|
+
Number.isInteger(value) && value > 0 ? Number(value) : undefined;
|
|
447
|
+
const sliceEntries = (entries) => {
|
|
448
|
+
const normalizedLimit = normalize(limit);
|
|
449
|
+
return normalizedLimit ? entries.slice(-normalizedLimit) : entries;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const consoleMessages = consoleMessagesByTabId.get(tabId) || [];
|
|
453
|
+
const pageErrors = pageErrorsByTabId.get(tabId) || [];
|
|
454
|
+
const networkFailures = networkFailuresByTabId.get(tabId) || [];
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
console_messages: sliceEntries(consoleMessages),
|
|
458
|
+
console_total: consoleMessages.length,
|
|
459
|
+
page_errors: sliceEntries(pageErrors),
|
|
460
|
+
page_error_total: pageErrors.length,
|
|
461
|
+
network_failures: sliceEntries(networkFailures),
|
|
462
|
+
network_failure_total: networkFailures.length,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function clearBufferedLogs(tabId) {
|
|
467
|
+
const consoleMessages = consoleMessagesByTabId.get(tabId) || [];
|
|
468
|
+
const pageErrors = pageErrorsByTabId.get(tabId) || [];
|
|
469
|
+
const networkFailures = networkFailuresByTabId.get(tabId) || [];
|
|
470
|
+
consoleMessagesByTabId.delete(tabId);
|
|
471
|
+
pageErrorsByTabId.delete(tabId);
|
|
472
|
+
networkFailuresByTabId.delete(tabId);
|
|
473
|
+
return {
|
|
474
|
+
console_messages_cleared: consoleMessages.length,
|
|
475
|
+
page_errors_cleared: pageErrors.length,
|
|
476
|
+
network_failures_cleared: networkFailures.length,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function executeTabActionInPage(action, payload) {
|
|
481
|
+
payload = payload || {};
|
|
482
|
+
const normalize = (value) => typeof value === "string" ? value.trim() : "";
|
|
483
|
+
const timeoutMs = Number.isInteger(payload.timeout_ms) ? payload.timeout_ms : 30000;
|
|
484
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
485
|
+
const byText = (text, exact) => {
|
|
486
|
+
const needle = normalize(text);
|
|
487
|
+
if (!needle) return null;
|
|
488
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
489
|
+
while (walker.nextNode()) {
|
|
490
|
+
const element = walker.currentNode;
|
|
491
|
+
const textContent = normalize(element.textContent || "");
|
|
492
|
+
if (!textContent) continue;
|
|
493
|
+
if ((exact && textContent === needle) || (!exact && textContent.includes(needle))) {
|
|
494
|
+
return element;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
};
|
|
499
|
+
const isVisible = (element) => {
|
|
500
|
+
if (!element) return false;
|
|
501
|
+
const computed = window.getComputedStyle(element);
|
|
502
|
+
return computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0";
|
|
503
|
+
};
|
|
504
|
+
const resolveTarget = () => {
|
|
505
|
+
const aiTag = normalize(payload.ai_tag);
|
|
506
|
+
if (aiTag) {
|
|
507
|
+
return document.querySelector('[data-drive-tag="' + aiTag.replace(/"/g, '\\"') + '"], [ai-tag="' + aiTag.replace(/"/g, '\\"') + '"]');
|
|
508
|
+
}
|
|
509
|
+
const selector = normalize(payload.selector);
|
|
510
|
+
if (selector) {
|
|
511
|
+
return document.querySelector(selector);
|
|
512
|
+
}
|
|
513
|
+
const text = normalize(payload.text);
|
|
514
|
+
if (text) {
|
|
515
|
+
return byText(text, payload.exact === true);
|
|
516
|
+
}
|
|
517
|
+
return document.body;
|
|
518
|
+
};
|
|
519
|
+
const target = resolveTarget();
|
|
520
|
+
if (!target) {
|
|
521
|
+
return { ok: false, error: "Target element was not found." };
|
|
522
|
+
}
|
|
523
|
+
if (target && typeof target.scrollIntoView === "function") {
|
|
524
|
+
target.scrollIntoView({ block: "center", inline: "center" });
|
|
525
|
+
}
|
|
526
|
+
if (action === "dom") {
|
|
527
|
+
const attributes = target
|
|
528
|
+
? Object.fromEntries(Array.from(target.attributes || []).map((attr) => [attr.name, attr.value]))
|
|
529
|
+
: {};
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
result: {
|
|
533
|
+
found: Boolean(target),
|
|
534
|
+
outer_html: payload.include_html === false ? undefined : target.outerHTML,
|
|
535
|
+
text_content: payload.include_text === false ? undefined : (target.textContent || "").trim(),
|
|
536
|
+
visible: target ? isVisible(target) : false,
|
|
537
|
+
attributes,
|
|
538
|
+
url: location.href,
|
|
539
|
+
title: document.title,
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (action === "computed_style") {
|
|
544
|
+
if (!target) {
|
|
545
|
+
return {
|
|
546
|
+
ok: true,
|
|
547
|
+
result: {
|
|
548
|
+
found: false,
|
|
549
|
+
url: location.href,
|
|
550
|
+
title: document.title,
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
const requestedProperties = Array.isArray(payload.properties) && payload.properties.length
|
|
555
|
+
? payload.properties.map((item) => String(item))
|
|
556
|
+
: ["display","position","visibility","opacity","color","background-color","font-size","z-index","overflow"];
|
|
557
|
+
const computed = window.getComputedStyle(target);
|
|
558
|
+
const rect = target.getBoundingClientRect();
|
|
559
|
+
const styles = Object.fromEntries(
|
|
560
|
+
requestedProperties.map((property) => [property, computed.getPropertyValue(property)]),
|
|
561
|
+
);
|
|
562
|
+
return {
|
|
563
|
+
ok: true,
|
|
564
|
+
result: {
|
|
565
|
+
found: true,
|
|
566
|
+
visible: isVisible(target),
|
|
567
|
+
styles,
|
|
568
|
+
box: {
|
|
569
|
+
x: rect.x,
|
|
570
|
+
y: rect.y,
|
|
571
|
+
width: rect.width,
|
|
572
|
+
height: rect.height,
|
|
573
|
+
},
|
|
574
|
+
url: location.href,
|
|
575
|
+
title: document.title,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (action === "click") {
|
|
580
|
+
target.click();
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
result: {
|
|
584
|
+
url: location.href,
|
|
585
|
+
title: document.title,
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (action === "wait_for") {
|
|
590
|
+
const waitState = normalize(payload.state) || "visible";
|
|
591
|
+
const startedAt = Date.now();
|
|
592
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
593
|
+
const current = resolveTarget();
|
|
594
|
+
const visible = current ? isVisible(current) : false;
|
|
595
|
+
if (
|
|
596
|
+
(waitState === "attached" && current) ||
|
|
597
|
+
(waitState === "detached" && !current) ||
|
|
598
|
+
(waitState === "visible" && current && visible) ||
|
|
599
|
+
(waitState === "hidden" && (!current || !visible))
|
|
600
|
+
) {
|
|
601
|
+
return {
|
|
602
|
+
ok: true,
|
|
603
|
+
result: {
|
|
604
|
+
url: location.href,
|
|
605
|
+
title: document.title,
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
await sleep(100);
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
error: "Timed out waiting for the requested element state.",
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
if (action === "wait_for_url") {
|
|
617
|
+
const exactUrl = normalize(payload.url);
|
|
618
|
+
const containsUrl = normalize(payload.url_contains);
|
|
619
|
+
const startedAt = Date.now();
|
|
620
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
621
|
+
const currentUrl = String(location.href || "");
|
|
622
|
+
if ((exactUrl && currentUrl === exactUrl) || (containsUrl && currentUrl.includes(containsUrl))) {
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
result: {
|
|
626
|
+
url: currentUrl,
|
|
627
|
+
title: document.title,
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
await sleep(100);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
ok: false,
|
|
635
|
+
error: "Timed out waiting for the requested URL.",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (action === "reload") {
|
|
639
|
+
location.reload();
|
|
640
|
+
return {
|
|
641
|
+
ok: true,
|
|
642
|
+
result: {
|
|
643
|
+
url: location.href,
|
|
644
|
+
title: document.title,
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (action === "fill") {
|
|
649
|
+
target.focus();
|
|
650
|
+
target.value = payload.value || "";
|
|
651
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
652
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
653
|
+
return {
|
|
654
|
+
ok: true,
|
|
655
|
+
result: {
|
|
656
|
+
url: location.href,
|
|
657
|
+
title: document.title,
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (action === "press") {
|
|
662
|
+
const key = String(payload.key || "");
|
|
663
|
+
if (target && typeof target.focus === "function") {
|
|
664
|
+
target.focus();
|
|
665
|
+
}
|
|
666
|
+
const eventInit = { key, bubbles: true, cancelable: true };
|
|
667
|
+
const keyboardTarget = document.activeElement || target || document.body;
|
|
668
|
+
keyboardTarget.dispatchEvent(new KeyboardEvent("keydown", eventInit));
|
|
669
|
+
keyboardTarget.dispatchEvent(new KeyboardEvent("keypress", eventInit));
|
|
670
|
+
keyboardTarget.dispatchEvent(new KeyboardEvent("keyup", eventInit));
|
|
671
|
+
return {
|
|
672
|
+
ok: true,
|
|
673
|
+
result: {
|
|
674
|
+
url: location.href,
|
|
675
|
+
title: document.title,
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return { ok: false, error: "Unsupported tab action." };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function runTabAction(tabId, action, payload) {
|
|
683
|
+
const activeTab = await ensureTabIsActive(tabId);
|
|
684
|
+
|
|
685
|
+
if (action === "attach") {
|
|
686
|
+
const record = {
|
|
687
|
+
tab_id: activeTab.id,
|
|
688
|
+
window_id: activeTab.windowId,
|
|
689
|
+
active: activeTab.active === true,
|
|
690
|
+
title: activeTab.title || "",
|
|
691
|
+
url: activeTab.url || "",
|
|
692
|
+
status: activeTab.status || "",
|
|
693
|
+
};
|
|
694
|
+
await setAttachedTab(record);
|
|
695
|
+
try {
|
|
696
|
+
await injectRecorderContent(tabId);
|
|
697
|
+
} catch {
|
|
698
|
+
// ignore
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
ok: true,
|
|
702
|
+
result: {
|
|
703
|
+
url: record.url,
|
|
704
|
+
title: record.title,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (action === "detach") {
|
|
710
|
+
await setAttachedTab(null);
|
|
711
|
+
return {
|
|
712
|
+
ok: true,
|
|
713
|
+
result: {
|
|
714
|
+
url: activeTab.url || "",
|
|
715
|
+
title: activeTab.title || "",
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (action === "close") {
|
|
721
|
+
if (attachedTabId === tabId) {
|
|
722
|
+
await setAttachedTab(null);
|
|
723
|
+
}
|
|
724
|
+
await tabsRemove(tabId);
|
|
725
|
+
return {
|
|
726
|
+
ok: true,
|
|
727
|
+
result: {
|
|
728
|
+
url: activeTab.url || "",
|
|
729
|
+
title: activeTab.title || "",
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (action === "get_logs") {
|
|
735
|
+
return {
|
|
736
|
+
ok: true,
|
|
737
|
+
result: getBufferedLogSnapshot(
|
|
738
|
+
tabId,
|
|
739
|
+
Number.isInteger(payload?.limit) ? Number(payload.limit) : undefined,
|
|
740
|
+
),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (action === "clear_logs") {
|
|
745
|
+
return {
|
|
746
|
+
ok: true,
|
|
747
|
+
result: clearBufferedLogs(tabId),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (action === "screenshot") {
|
|
752
|
+
await new Promise((resolve) => setTimeout(resolve, 180));
|
|
753
|
+
const refreshedTab = await tabsGet(tabId).catch(() => activeTab);
|
|
754
|
+
const dataUrl = await captureVisibleTab(activeTab.windowId, {
|
|
755
|
+
format: "png",
|
|
756
|
+
});
|
|
757
|
+
return {
|
|
758
|
+
ok: true,
|
|
759
|
+
result: {
|
|
760
|
+
png_base64: String(dataUrl).replace(/^data:image\/png;base64,/, ""),
|
|
761
|
+
url: refreshedTab.url || "",
|
|
762
|
+
title: refreshedTab.title || "",
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (action === "inject_script") {
|
|
768
|
+
const namespace =
|
|
769
|
+
typeof payload?.namespace === "string" && payload.namespace.trim()
|
|
770
|
+
? payload.namespace.trim()
|
|
771
|
+
: "TELLY";
|
|
772
|
+
const source = String(payload?.source || "");
|
|
773
|
+
if (!source) {
|
|
774
|
+
return {
|
|
775
|
+
ok: false,
|
|
776
|
+
error: "Script source is required.",
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const execution = await scriptingExecuteScript({
|
|
781
|
+
target: { tabId },
|
|
782
|
+
world: "MAIN",
|
|
783
|
+
func: runInjectedMainWorldScript,
|
|
784
|
+
args: [namespace, source, collectInjectExportNames(source)],
|
|
785
|
+
});
|
|
786
|
+
const result = execution[0]?.result;
|
|
787
|
+
|
|
788
|
+
if (!result || result.ok !== true) {
|
|
789
|
+
return {
|
|
790
|
+
ok: false,
|
|
791
|
+
error: result?.error || "Tab action did not return a successful result.",
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const execution = await scriptingExecuteScript({
|
|
799
|
+
target: { tabId },
|
|
800
|
+
func: executeTabActionInPage,
|
|
801
|
+
args: [action, payload || {}],
|
|
802
|
+
});
|
|
803
|
+
const result = execution[0]?.result;
|
|
804
|
+
|
|
805
|
+
if (!result || result.ok !== true) {
|
|
806
|
+
return {
|
|
807
|
+
ok: false,
|
|
808
|
+
error: result?.error || "Tab action did not return a successful result.",
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function sendJson(payload) {
|
|
816
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
socket.send(JSON.stringify(payload));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function isSocketOpenOrConnecting() {
|
|
823
|
+
return Boolean(
|
|
824
|
+
socket &&
|
|
825
|
+
(socket.readyState === WebSocket.OPEN ||
|
|
826
|
+
socket.readyState === WebSocket.CONNECTING),
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function clearTimers() {
|
|
831
|
+
if (reconnectTimer) {
|
|
832
|
+
clearTimeout(reconnectTimer);
|
|
833
|
+
reconnectTimer = null;
|
|
834
|
+
}
|
|
835
|
+
if (heartbeatTimer) {
|
|
836
|
+
clearInterval(heartbeatTimer);
|
|
837
|
+
heartbeatTimer = null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function scheduleReconnect() {
|
|
842
|
+
if (manualDisconnect) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
clearTimers();
|
|
846
|
+
reconnectTimer = setTimeout(() => {
|
|
847
|
+
void connect();
|
|
848
|
+
}, RECONNECT_DELAY_MS);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function waitForSocketReady(timeoutMs = 2500) {
|
|
852
|
+
const deadline = Date.now() + timeoutMs;
|
|
853
|
+
while (Date.now() < deadline) {
|
|
854
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
858
|
+
}
|
|
859
|
+
return Boolean(socket && socket.readyState === WebSocket.OPEN);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function startHeartbeat() {
|
|
863
|
+
if (heartbeatTimer) {
|
|
864
|
+
clearInterval(heartbeatTimer);
|
|
865
|
+
}
|
|
866
|
+
heartbeatTimer = setInterval(() => {
|
|
867
|
+
sendJson({
|
|
868
|
+
type: "heartbeat",
|
|
869
|
+
sent_at: formatLocalTimestamp(new Date()),
|
|
870
|
+
});
|
|
871
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function sendHello() {
|
|
875
|
+
sendJson({
|
|
876
|
+
type: "hello",
|
|
877
|
+
extension_version: browser.runtime.getManifest().version,
|
|
878
|
+
browser: "chrome",
|
|
879
|
+
instance_id: await computeInstanceId(),
|
|
880
|
+
profile_name: "default",
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function bytesToBase64(bytes) {
|
|
885
|
+
let binary = "";
|
|
886
|
+
const chunkSize = 0x8000;
|
|
887
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
888
|
+
const slice = bytes.subarray(index, index + chunkSize);
|
|
889
|
+
binary += String.fromCharCode(...slice);
|
|
890
|
+
}
|
|
891
|
+
return btoa(binary);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function headersToArray(headers) {
|
|
895
|
+
return (headers || []).map((header) => ({
|
|
896
|
+
name: String(header.name || ""),
|
|
897
|
+
value: String(header.value || ""),
|
|
898
|
+
}));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function getRecordingByTabId(tabId) {
|
|
902
|
+
const recordingId = activeRecordingIdByTabId.get(tabId);
|
|
903
|
+
return recordingId ? activeRecordingsById.get(recordingId) || null : null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function sendRecordingEvent(recordingId, tabId, event) {
|
|
907
|
+
if (!recordingId || !Number.isInteger(tabId)) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
sendJson({
|
|
911
|
+
type: "recording_event",
|
|
912
|
+
recording_id: recordingId,
|
|
913
|
+
tab_id: tabId,
|
|
914
|
+
event: {
|
|
915
|
+
...event,
|
|
916
|
+
at: event.at || formatLocalTimestamp(new Date()),
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async function injectRecorderContent(tabId) {
|
|
922
|
+
await scriptingExecuteScript({
|
|
923
|
+
target: { tabId },
|
|
924
|
+
files: ["recorder-content.js"],
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function capturePageSnapshotInTab(recordingReason) {
|
|
929
|
+
const now = new Date();
|
|
930
|
+
const pad = (value, length = 2) => String(value).padStart(length, "0");
|
|
931
|
+
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(
|
|
932
|
+
now.getHours(),
|
|
933
|
+
)}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(
|
|
934
|
+
now.getMilliseconds(),
|
|
935
|
+
3,
|
|
936
|
+
)}`;
|
|
937
|
+
return {
|
|
938
|
+
kind: "page_snapshot",
|
|
939
|
+
source: "background",
|
|
940
|
+
reason: recordingReason,
|
|
941
|
+
at: timestamp,
|
|
942
|
+
url: location.href,
|
|
943
|
+
title: document.title,
|
|
944
|
+
ready_state: document.readyState,
|
|
945
|
+
html: document.documentElement ? document.documentElement.outerHTML : "",
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
async function captureTabSnapshot(tabId, reason) {
|
|
950
|
+
const execution = await scriptingExecuteScript({
|
|
951
|
+
target: { tabId },
|
|
952
|
+
func: capturePageSnapshotInTab,
|
|
953
|
+
args: [reason],
|
|
954
|
+
});
|
|
955
|
+
const result = execution[0]?.result;
|
|
956
|
+
return result || null;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function emitCookiesSnapshot(recordingId, tabId, tabUrl, tabTitle) {
|
|
960
|
+
if (!tabUrl || !/^https?:\/\//iu.test(tabUrl)) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
const cookies = await cookiesGetAll({ url: tabUrl });
|
|
965
|
+
sendRecordingEvent(recordingId, tabId, {
|
|
966
|
+
kind: "cookies_snapshot",
|
|
967
|
+
source: "browser",
|
|
968
|
+
url: tabUrl,
|
|
969
|
+
tab_title: tabTitle || "",
|
|
970
|
+
cookies: cookies.map((cookie) => ({
|
|
971
|
+
name: cookie.name,
|
|
972
|
+
value: cookie.value,
|
|
973
|
+
domain: cookie.domain,
|
|
974
|
+
path: cookie.path,
|
|
975
|
+
secure: cookie.secure,
|
|
976
|
+
http_only: cookie.httpOnly,
|
|
977
|
+
same_site: cookie.sameSite,
|
|
978
|
+
store_id: cookie.storeId,
|
|
979
|
+
})),
|
|
980
|
+
});
|
|
981
|
+
} catch {
|
|
982
|
+
// ignore
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function startRecording(message) {
|
|
987
|
+
const attached = (await storageGet({ [ATTACHED_TAB_KEY]: null }))[ATTACHED_TAB_KEY];
|
|
988
|
+
const tabId = Number(message.tab_id);
|
|
989
|
+
if (!attached || attached.tab_id !== tabId) {
|
|
990
|
+
return {
|
|
991
|
+
ok: false,
|
|
992
|
+
active: false,
|
|
993
|
+
error: "Selected attached tab does not match the requested recording tab.",
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const browserTab = await tabsGet(tabId);
|
|
998
|
+
activeRecordingsById.set(message.recording_id, {
|
|
999
|
+
recordingId: message.recording_id,
|
|
1000
|
+
tabId,
|
|
1001
|
+
tabTitle: browserTab.title || "",
|
|
1002
|
+
tabUrl: browserTab.url || "",
|
|
1003
|
+
startedAt: formatLocalTimestamp(new Date()),
|
|
1004
|
+
});
|
|
1005
|
+
activeRecordingIdByTabId.set(tabId, message.recording_id);
|
|
1006
|
+
|
|
1007
|
+
await injectRecorderContent(tabId);
|
|
1008
|
+
const snapshot = await captureTabSnapshot(tabId, "recording-start");
|
|
1009
|
+
if (snapshot) {
|
|
1010
|
+
sendRecordingEvent(message.recording_id, tabId, snapshot);
|
|
1011
|
+
}
|
|
1012
|
+
await emitCookiesSnapshot(
|
|
1013
|
+
message.recording_id,
|
|
1014
|
+
tabId,
|
|
1015
|
+
browserTab.url || "",
|
|
1016
|
+
browserTab.title || "",
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
return {
|
|
1020
|
+
ok: true,
|
|
1021
|
+
active: true,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function stopRecording(message) {
|
|
1026
|
+
activeRecordingsById.delete(message.recording_id);
|
|
1027
|
+
activeRecordingIdByTabId.delete(message.tab_id);
|
|
1028
|
+
await setRecordingStatus({
|
|
1029
|
+
active: false,
|
|
1030
|
+
});
|
|
1031
|
+
return {
|
|
1032
|
+
ok: true,
|
|
1033
|
+
active: false,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function handleAttachTabSelectedCommand(tabId) {
|
|
1038
|
+
if (!Number.isInteger(tabId) || tabId < 0) {
|
|
1039
|
+
return {
|
|
1040
|
+
ok: false,
|
|
1041
|
+
error: "Invalid tab_id.",
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
let browserTab;
|
|
1046
|
+
try {
|
|
1047
|
+
browserTab = await tabsGet(tabId);
|
|
1048
|
+
} catch {
|
|
1049
|
+
return {
|
|
1050
|
+
ok: false,
|
|
1051
|
+
error: "Tab not found.",
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const ready = await waitForSocketReady();
|
|
1056
|
+
if (!ready) {
|
|
1057
|
+
return {
|
|
1058
|
+
ok: false,
|
|
1059
|
+
error: "Extension is not connected to TellyMCP.",
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const record = {
|
|
1064
|
+
tab_id: tabId,
|
|
1065
|
+
window_id: browserTab.windowId,
|
|
1066
|
+
active: browserTab.active === true,
|
|
1067
|
+
title: browserTab.title || "",
|
|
1068
|
+
url: browserTab.url || "",
|
|
1069
|
+
status: browserTab.status || "",
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
sendJson({
|
|
1073
|
+
type: "attach_tab_selected",
|
|
1074
|
+
tab: record,
|
|
1075
|
+
});
|
|
1076
|
+
await setAttachedTab(record);
|
|
1077
|
+
try {
|
|
1078
|
+
await injectRecorderContent(tabId);
|
|
1079
|
+
} catch {
|
|
1080
|
+
// ignore recorder bootstrap errors for manual attach
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return {
|
|
1084
|
+
ok: true,
|
|
1085
|
+
tab: record,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function handlePopupCommand(command) {
|
|
1090
|
+
if (!command || typeof command !== "object") {
|
|
1091
|
+
return {
|
|
1092
|
+
ok: false,
|
|
1093
|
+
error: "Invalid popup command.",
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (command.type === "attach_tab_selected") {
|
|
1098
|
+
return await handleAttachTabSelectedCommand(Number(command.tab_id));
|
|
1099
|
+
}
|
|
1100
|
+
if (command.type === "attach_recording_start") {
|
|
1101
|
+
const stored = await storageGet({ [ATTACHED_TAB_KEY]: null });
|
|
1102
|
+
const tab = stored[ATTACHED_TAB_KEY];
|
|
1103
|
+
if (!tab) {
|
|
1104
|
+
return {
|
|
1105
|
+
ok: false,
|
|
1106
|
+
active: false,
|
|
1107
|
+
error: "Select a tab first.",
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
return await sendManualRecordingRequest("recording_manual_start", { tab });
|
|
1111
|
+
}
|
|
1112
|
+
if (command.type === "attach_recording_stop") {
|
|
1113
|
+
return await sendManualRecordingRequest("recording_manual_stop");
|
|
1114
|
+
}
|
|
1115
|
+
if (command.type === "attach_recording_status") {
|
|
1116
|
+
return await sendManualRecordingRequest("recording_manual_status");
|
|
1117
|
+
}
|
|
1118
|
+
if (command.type === "attach_inject_script") {
|
|
1119
|
+
const stored = await storageGet({ [ATTACHED_TAB_KEY]: null });
|
|
1120
|
+
const tab = stored[ATTACHED_TAB_KEY];
|
|
1121
|
+
if (!tab) {
|
|
1122
|
+
return {
|
|
1123
|
+
ok: false,
|
|
1124
|
+
error: "Select a tab first.",
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
const source = String(command.source || "");
|
|
1128
|
+
if (!source.trim()) {
|
|
1129
|
+
return {
|
|
1130
|
+
ok: false,
|
|
1131
|
+
error: "Script source is empty.",
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
return await runTabAction(tab.tab_id, "inject_script", {
|
|
1135
|
+
namespace: typeof command.namespace === "string" ? command.namespace : "TELLY",
|
|
1136
|
+
source,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
ok: false,
|
|
1142
|
+
error: `Unsupported popup command: ${String(command.type || "")}`,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async function sendManualRecordingRequest(type, payload = {}) {
|
|
1147
|
+
const ready = await waitForSocketReady();
|
|
1148
|
+
if (!ready) {
|
|
1149
|
+
return {
|
|
1150
|
+
ok: false,
|
|
1151
|
+
active: false,
|
|
1152
|
+
error: "Extension is not connected to TellyMCP.",
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const requestId = `manual-recording-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1157
|
+
const result = await new Promise((resolve, reject) => {
|
|
1158
|
+
const timer = setTimeout(() => {
|
|
1159
|
+
pendingManualRecordingRequests.delete(requestId);
|
|
1160
|
+
reject(new Error("Recording request timed out."));
|
|
1161
|
+
}, 15000);
|
|
1162
|
+
pendingManualRecordingRequests.set(requestId, { resolve, reject, timer });
|
|
1163
|
+
sendJson({
|
|
1164
|
+
type,
|
|
1165
|
+
request_id: requestId,
|
|
1166
|
+
...payload,
|
|
1167
|
+
});
|
|
1168
|
+
}).catch((error) => ({
|
|
1169
|
+
ok: false,
|
|
1170
|
+
active: false,
|
|
1171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1172
|
+
}));
|
|
1173
|
+
|
|
1174
|
+
if (result && typeof result === "object" && result.recording) {
|
|
1175
|
+
await setRecordingStatus(result);
|
|
1176
|
+
} else if (result && typeof result === "object" && result.active === false) {
|
|
1177
|
+
await setRecordingStatus({ active: false });
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return result;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function handleTabActionMessage(message) {
|
|
1184
|
+
let result;
|
|
1185
|
+
try {
|
|
1186
|
+
result = await runTabAction(
|
|
1187
|
+
message.tab_id,
|
|
1188
|
+
message.action,
|
|
1189
|
+
message.payload || {},
|
|
1190
|
+
);
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
result = {
|
|
1193
|
+
ok: false,
|
|
1194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
sendJson({
|
|
1198
|
+
type: "tab_action_result",
|
|
1199
|
+
request_id: message.request_id,
|
|
1200
|
+
ok: result.ok === true,
|
|
1201
|
+
...(result.result ? { result: result.result } : {}),
|
|
1202
|
+
...(result.error ? { error: result.error } : {}),
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function handleMessage(rawData) {
|
|
1207
|
+
const message =
|
|
1208
|
+
typeof rawData === "string"
|
|
1209
|
+
? JSON.parse(rawData)
|
|
1210
|
+
: JSON.parse(String(rawData));
|
|
1211
|
+
|
|
1212
|
+
switch (message.type) {
|
|
1213
|
+
case "hello_ack":
|
|
1214
|
+
await setConnectionStatus({
|
|
1215
|
+
state: "connected",
|
|
1216
|
+
text: `Connected: ${message.session_label || message.session_id || message.instance_id}`,
|
|
1217
|
+
instance_id: message.instance_id,
|
|
1218
|
+
...(message.session_id ? { session_id: message.session_id } : {}),
|
|
1219
|
+
...(message.session_label ? { session_label: message.session_label } : {}),
|
|
1220
|
+
});
|
|
1221
|
+
startHeartbeat();
|
|
1222
|
+
return;
|
|
1223
|
+
case "list_tabs": {
|
|
1224
|
+
sendJson({
|
|
1225
|
+
type: "list_tabs_result",
|
|
1226
|
+
request_id: message.request_id,
|
|
1227
|
+
tabs: await listTabs(),
|
|
1228
|
+
});
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
case "get_active_tab": {
|
|
1232
|
+
sendJson({
|
|
1233
|
+
type: "get_active_tab_result",
|
|
1234
|
+
request_id: message.request_id,
|
|
1235
|
+
tab: await getActiveTab(),
|
|
1236
|
+
});
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
case "tab_action": {
|
|
1240
|
+
await handleTabActionMessage(message);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
case "recording_start": {
|
|
1244
|
+
const result = await startRecording(message);
|
|
1245
|
+
sendJson({
|
|
1246
|
+
type: "recording_control_result",
|
|
1247
|
+
request_id: message.request_id,
|
|
1248
|
+
ok: result.ok === true,
|
|
1249
|
+
active: result.active === true,
|
|
1250
|
+
...(result.error ? { error: result.error } : {}),
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
case "recording_stop": {
|
|
1255
|
+
const result = await stopRecording(message);
|
|
1256
|
+
sendJson({
|
|
1257
|
+
type: "recording_control_result",
|
|
1258
|
+
request_id: message.request_id,
|
|
1259
|
+
ok: result.ok === true,
|
|
1260
|
+
active: result.active === true,
|
|
1261
|
+
...(result.error ? { error: result.error } : {}),
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
case "recording_manual_result": {
|
|
1266
|
+
const pending = pendingManualRecordingRequests.get(message.request_id);
|
|
1267
|
+
if (!pending) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
clearTimeout(pending.timer);
|
|
1271
|
+
pendingManualRecordingRequests.delete(message.request_id);
|
|
1272
|
+
pending.resolve(message);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
case "recording_state":
|
|
1276
|
+
await setRecordingStatus({
|
|
1277
|
+
active: message.active === true,
|
|
1278
|
+
...(message.recording ? { recording: message.recording } : {}),
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
default:
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function connect() {
|
|
1287
|
+
const settings = await getSettings();
|
|
1288
|
+
manualDisconnect = settings[CONNECTION_ENABLED_KEY] === false;
|
|
1289
|
+
if (manualDisconnect) {
|
|
1290
|
+
clearTimers();
|
|
1291
|
+
await setConnectionStatus({
|
|
1292
|
+
state: "disconnected",
|
|
1293
|
+
text: "Disconnected: manual",
|
|
1294
|
+
});
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (isSocketOpenOrConnecting()) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
clearTimers();
|
|
1301
|
+
await setConnectionStatus({
|
|
1302
|
+
state: "connecting",
|
|
1303
|
+
text: `Connecting: ${settings.host}:${settings.port}`,
|
|
1304
|
+
});
|
|
1305
|
+
const wsUrl = buildWebSocketUrl(settings);
|
|
1306
|
+
socket = new WebSocket(wsUrl);
|
|
1307
|
+
|
|
1308
|
+
socket.addEventListener("open", () => {
|
|
1309
|
+
void sendHello();
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
socket.addEventListener("message", (event) => {
|
|
1313
|
+
void handleMessage(event.data);
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
socket.addEventListener("close", () => {
|
|
1317
|
+
socket = null;
|
|
1318
|
+
if (manualDisconnect) {
|
|
1319
|
+
void setConnectionStatus({
|
|
1320
|
+
state: "disconnected",
|
|
1321
|
+
text: "Disconnected: manual",
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
void setConnectionStatus({
|
|
1326
|
+
state: "disconnected",
|
|
1327
|
+
text: `Disconnected: reconnecting in ${Math.floor(RECONNECT_DELAY_MS / 1000)}s`,
|
|
1328
|
+
});
|
|
1329
|
+
scheduleReconnect();
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
socket.addEventListener("error", () => {
|
|
1333
|
+
void setConnectionStatus({
|
|
1334
|
+
state: "disconnected",
|
|
1335
|
+
text: "Disconnected: WebSocket error",
|
|
1336
|
+
});
|
|
1337
|
+
if (socket) {
|
|
1338
|
+
socket.close();
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
browser.storage.onChanged.addListener((changes, areaName) => {
|
|
1344
|
+
if (areaName !== "local") {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (changes[CONNECTION_ENABLED_KEY] !== undefined) {
|
|
1349
|
+
const nextEnabled = changes[CONNECTION_ENABLED_KEY].newValue !== false;
|
|
1350
|
+
manualDisconnect = !nextEnabled;
|
|
1351
|
+
if (nextEnabled) {
|
|
1352
|
+
if (socket) {
|
|
1353
|
+
socket.close();
|
|
1354
|
+
} else {
|
|
1355
|
+
void connect();
|
|
1356
|
+
}
|
|
1357
|
+
} else {
|
|
1358
|
+
clearTimers();
|
|
1359
|
+
if (socket) {
|
|
1360
|
+
socket.close();
|
|
1361
|
+
} else {
|
|
1362
|
+
void setConnectionStatus({
|
|
1363
|
+
state: "disconnected",
|
|
1364
|
+
text: "Disconnected: manual",
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (changes[POPUP_COMMAND_KEY] !== undefined) {
|
|
1372
|
+
const command = changes[POPUP_COMMAND_KEY].newValue;
|
|
1373
|
+
if (!command) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
void handlePopupCommand(command)
|
|
1377
|
+
.then((result) =>
|
|
1378
|
+
storageSet({
|
|
1379
|
+
[POPUP_COMMAND_RESULT_KEY]: {
|
|
1380
|
+
command_id: command.command_id,
|
|
1381
|
+
result,
|
|
1382
|
+
at: formatLocalTimestamp(new Date()),
|
|
1383
|
+
},
|
|
1384
|
+
}),
|
|
1385
|
+
)
|
|
1386
|
+
.catch((error) =>
|
|
1387
|
+
storageSet({
|
|
1388
|
+
[POPUP_COMMAND_RESULT_KEY]: {
|
|
1389
|
+
command_id: command.command_id,
|
|
1390
|
+
result: {
|
|
1391
|
+
ok: false,
|
|
1392
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1393
|
+
},
|
|
1394
|
+
at: formatLocalTimestamp(new Date()),
|
|
1395
|
+
},
|
|
1396
|
+
}),
|
|
1397
|
+
);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (
|
|
1402
|
+
changes.host === undefined &&
|
|
1403
|
+
changes.port === undefined
|
|
1404
|
+
) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (socket) {
|
|
1409
|
+
socket.close();
|
|
1410
|
+
} else if (!manualDisconnect) {
|
|
1411
|
+
void connect();
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
browser.runtime.onConnect.addListener((port) => {
|
|
1416
|
+
if (port.name !== CONTROL_PANEL_PORT_NAME) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
controlPanelPorts.add(port);
|
|
1420
|
+
port.onDisconnect.addListener(() => {
|
|
1421
|
+
controlPanelPorts.delete(port);
|
|
1422
|
+
});
|
|
1423
|
+
port.onMessage.addListener(() => {
|
|
1424
|
+
// keep the service worker alive while the control panel is open
|
|
1425
|
+
});
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
browser.tabs.onActivated.addListener(async () => {
|
|
1429
|
+
const tab = await getActiveTab();
|
|
1430
|
+
if (!tab) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
sendJson({
|
|
1434
|
+
type: "active_tab_changed",
|
|
1435
|
+
tab,
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|
1440
|
+
if (
|
|
1441
|
+
typeof tabId !== "number" ||
|
|
1442
|
+
(changeInfo.title === undefined &&
|
|
1443
|
+
changeInfo.url === undefined &&
|
|
1444
|
+
changeInfo.status === undefined)
|
|
1445
|
+
) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
sendJson({
|
|
1450
|
+
type: "tab_updated",
|
|
1451
|
+
tab: {
|
|
1452
|
+
tab_id: tabId,
|
|
1453
|
+
window_id: tab.windowId,
|
|
1454
|
+
active: tab.active === true,
|
|
1455
|
+
title: tab.title || "",
|
|
1456
|
+
url: tab.url || "",
|
|
1457
|
+
status: changeInfo.status || tab.status || "",
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
const recording = getRecordingByTabId(tabId);
|
|
1462
|
+
const shouldTrackTab = recording || attachedTabId === tabId;
|
|
1463
|
+
if (!shouldTrackTab) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (recording) {
|
|
1468
|
+
sendRecordingEvent(recording.recordingId, tabId, {
|
|
1469
|
+
kind: "navigation",
|
|
1470
|
+
source: "browser",
|
|
1471
|
+
status: changeInfo.status || tab.status || "",
|
|
1472
|
+
url: tab.url || "",
|
|
1473
|
+
title: tab.title || "",
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (changeInfo.status === "complete") {
|
|
1478
|
+
try {
|
|
1479
|
+
await injectRecorderContent(tabId);
|
|
1480
|
+
if (recording) {
|
|
1481
|
+
const snapshot = await captureTabSnapshot(tabId, "tab-updated-complete");
|
|
1482
|
+
if (snapshot) {
|
|
1483
|
+
sendRecordingEvent(recording.recordingId, tabId, snapshot);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
} catch {
|
|
1487
|
+
// ignore
|
|
1488
|
+
}
|
|
1489
|
+
if (recording) {
|
|
1490
|
+
await emitCookiesSnapshot(
|
|
1491
|
+
recording.recordingId,
|
|
1492
|
+
tabId,
|
|
1493
|
+
tab.url || "",
|
|
1494
|
+
tab.title || "",
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
async function handleRuntimeMessage(message, sender) {
|
|
1501
|
+
if (!message || typeof message !== "object") {
|
|
1502
|
+
return undefined;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
if (message.type === "attach_tab_selected") {
|
|
1506
|
+
return await handleAttachTabSelectedCommand(Number(message.tab_id));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (message.type === "attach_connection_set_enabled") {
|
|
1510
|
+
return (async () => {
|
|
1511
|
+
const enabled = message.enabled === true;
|
|
1512
|
+
await storageSet({
|
|
1513
|
+
[CONNECTION_ENABLED_KEY]: enabled,
|
|
1514
|
+
});
|
|
1515
|
+
return { ok: true, enabled };
|
|
1516
|
+
})();
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if (message.type === "attach_recording_start") {
|
|
1520
|
+
return await handlePopupCommand({ type: "attach_recording_start" });
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (message.type === "attach_recording_stop") {
|
|
1524
|
+
return await handlePopupCommand({ type: "attach_recording_stop" });
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (message.type === "attach_recording_status") {
|
|
1528
|
+
return await handlePopupCommand({ type: "attach_recording_status" });
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (message.type === "attach_inject_script") {
|
|
1532
|
+
return await handlePopupCommand({
|
|
1533
|
+
type: "attach_inject_script",
|
|
1534
|
+
source: message.source,
|
|
1535
|
+
namespace: message.namespace,
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (message.type === "telly_recording_page_event") {
|
|
1540
|
+
const tabId = sender?.tab?.id;
|
|
1541
|
+
if (!Number.isInteger(tabId)) {
|
|
1542
|
+
return undefined;
|
|
1543
|
+
}
|
|
1544
|
+
recordPageEventForBuffers(tabId, message.event || {});
|
|
1545
|
+
const recording = getRecordingByTabId(tabId);
|
|
1546
|
+
if (!recording) {
|
|
1547
|
+
return { ok: true };
|
|
1548
|
+
}
|
|
1549
|
+
sendRecordingEvent(recording.recordingId, tabId, message.event || {});
|
|
1550
|
+
return { ok: true };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return undefined;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1557
|
+
void handleRuntimeMessage(message, sender)
|
|
1558
|
+
.then((result) => {
|
|
1559
|
+
sendResponse(result);
|
|
1560
|
+
})
|
|
1561
|
+
.catch((error) => {
|
|
1562
|
+
sendResponse({
|
|
1563
|
+
ok: false,
|
|
1564
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1565
|
+
});
|
|
1566
|
+
});
|
|
1567
|
+
return true;
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
browser.webRequest.onBeforeRequest.addListener(
|
|
1571
|
+
(details) => {
|
|
1572
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1573
|
+
if (!recording) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
let bodyText = "";
|
|
1578
|
+
let bodyBase64 = "";
|
|
1579
|
+
if (details.requestBody?.formData) {
|
|
1580
|
+
bodyText = Object.entries(details.requestBody.formData)
|
|
1581
|
+
.map(([name, values]) =>
|
|
1582
|
+
values.map((value) => `${name}=${String(value)}`).join("&"),
|
|
1583
|
+
)
|
|
1584
|
+
.join("&");
|
|
1585
|
+
} else if (Array.isArray(details.requestBody?.raw)) {
|
|
1586
|
+
const chunks = details.requestBody.raw
|
|
1587
|
+
.map((item) => item.bytes)
|
|
1588
|
+
.filter(Boolean)
|
|
1589
|
+
.map((buffer) => new Uint8Array(buffer));
|
|
1590
|
+
if (chunks.length > 0) {
|
|
1591
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1592
|
+
const merged = new Uint8Array(total);
|
|
1593
|
+
let offset = 0;
|
|
1594
|
+
for (const chunk of chunks) {
|
|
1595
|
+
merged.set(chunk, offset);
|
|
1596
|
+
offset += chunk.length;
|
|
1597
|
+
}
|
|
1598
|
+
bodyBase64 = bytesToBase64(merged.subarray(0, MAX_CAPTURE_BYTES));
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1603
|
+
kind: "network_request",
|
|
1604
|
+
source: "browser",
|
|
1605
|
+
request_id: details.requestId,
|
|
1606
|
+
url: details.url,
|
|
1607
|
+
method: details.method,
|
|
1608
|
+
resource_type: details.type,
|
|
1609
|
+
...(bodyText ? { body_text: bodyText.slice(0, MAX_CAPTURE_BYTES) } : {}),
|
|
1610
|
+
...(bodyBase64 ? { body_base64: bodyBase64 } : {}),
|
|
1611
|
+
});
|
|
1612
|
+
},
|
|
1613
|
+
{ urls: ["<all_urls>"] },
|
|
1614
|
+
["requestBody"],
|
|
1615
|
+
);
|
|
1616
|
+
|
|
1617
|
+
browser.webRequest.onBeforeSendHeaders.addListener(
|
|
1618
|
+
(details) => {
|
|
1619
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1620
|
+
if (!recording) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1624
|
+
kind: "network_request_headers",
|
|
1625
|
+
source: "browser",
|
|
1626
|
+
request_id: details.requestId,
|
|
1627
|
+
url: details.url,
|
|
1628
|
+
method: details.method,
|
|
1629
|
+
resource_type: details.type,
|
|
1630
|
+
headers: headersToArray(details.requestHeaders),
|
|
1631
|
+
});
|
|
1632
|
+
},
|
|
1633
|
+
{ urls: ["<all_urls>"] },
|
|
1634
|
+
["requestHeaders"],
|
|
1635
|
+
);
|
|
1636
|
+
|
|
1637
|
+
browser.webRequest.onHeadersReceived.addListener(
|
|
1638
|
+
(details) => {
|
|
1639
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1640
|
+
if (!recording) {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1645
|
+
kind: "network_response_headers",
|
|
1646
|
+
source: "browser",
|
|
1647
|
+
request_id: details.requestId,
|
|
1648
|
+
url: details.url,
|
|
1649
|
+
method: details.method,
|
|
1650
|
+
resource_type: details.type,
|
|
1651
|
+
status_code: details.statusCode,
|
|
1652
|
+
headers: headersToArray(details.responseHeaders),
|
|
1653
|
+
});
|
|
1654
|
+
},
|
|
1655
|
+
{ urls: ["<all_urls>"] },
|
|
1656
|
+
["responseHeaders"],
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
browser.webRequest.onCompleted.addListener(
|
|
1660
|
+
(details) => {
|
|
1661
|
+
if (
|
|
1662
|
+
Number.isInteger(details.tabId) &&
|
|
1663
|
+
details.tabId >= 0 &&
|
|
1664
|
+
typeof details.statusCode === "number" &&
|
|
1665
|
+
details.statusCode >= 400
|
|
1666
|
+
) {
|
|
1667
|
+
recordNetworkFailure(details.tabId, {
|
|
1668
|
+
url: details.url,
|
|
1669
|
+
method: details.method,
|
|
1670
|
+
status: details.statusCode,
|
|
1671
|
+
resource_type: details.type,
|
|
1672
|
+
timestamp: formatLocalTimestamp(new Date()),
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1676
|
+
if (!recording) {
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1680
|
+
kind: "network_response_complete",
|
|
1681
|
+
source: "browser",
|
|
1682
|
+
request_id: details.requestId,
|
|
1683
|
+
url: details.url,
|
|
1684
|
+
method: details.method,
|
|
1685
|
+
resource_type: details.type,
|
|
1686
|
+
status_code: details.statusCode,
|
|
1687
|
+
});
|
|
1688
|
+
},
|
|
1689
|
+
{ urls: ["<all_urls>"] },
|
|
1690
|
+
);
|
|
1691
|
+
|
|
1692
|
+
browser.webRequest.onErrorOccurred.addListener(
|
|
1693
|
+
(details) => {
|
|
1694
|
+
if (Number.isInteger(details.tabId) && details.tabId >= 0) {
|
|
1695
|
+
recordNetworkFailure(details.tabId, {
|
|
1696
|
+
url: details.url,
|
|
1697
|
+
method: details.method,
|
|
1698
|
+
error_text: details.error,
|
|
1699
|
+
resource_type: details.type,
|
|
1700
|
+
timestamp: formatLocalTimestamp(new Date()),
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
const recording = getRecordingByTabId(details.tabId);
|
|
1704
|
+
if (!recording) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
sendRecordingEvent(recording.recordingId, details.tabId, {
|
|
1708
|
+
kind: "network_error",
|
|
1709
|
+
source: "browser",
|
|
1710
|
+
request_id: details.requestId,
|
|
1711
|
+
url: details.url,
|
|
1712
|
+
method: details.method,
|
|
1713
|
+
resource_type: details.type,
|
|
1714
|
+
error: details.error,
|
|
1715
|
+
});
|
|
1716
|
+
},
|
|
1717
|
+
{ urls: ["<all_urls>"] },
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
browser.alarms.onAlarm.addListener((alarm) => {
|
|
1721
|
+
if (alarm.name !== KEEPALIVE_ALARM_NAME || manualDisconnect) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (!isSocketOpenOrConnecting()) {
|
|
1725
|
+
void connect();
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
browser.alarms.create(KEEPALIVE_ALARM_NAME, {
|
|
1730
|
+
periodInMinutes: KEEPALIVE_ALARM_PERIOD_MINUTES,
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
void hydrateAttachedTabSelection();
|
|
1734
|
+
void computeInstanceId();
|
|
1735
|
+
void connect();
|