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