@deadragdoll/tellymcp 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-ru.md +19 -0
- package/README.md +19 -0
- package/TOOLS.md +206 -3
- package/dist/cli.js +109 -1
- package/dist/services/features/telegram-mcp/browser.service.js +38 -1
- package/dist/services/features/telegram-mcp/mcp-server.service.js +12 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +14 -0
- package/dist/services/features/telegram-mcp/src/app/config/env.js +13 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +147 -2
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +485 -1
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +502 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +79 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +559 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
- package/docs/STANDALONE-ru.md +42 -6
- package/docs/STANDALONE.md +42 -6
- package/package.json +6 -3
- package/packages/chrome-attach-extension/dist/background.js +1326 -0
- package/packages/chrome-attach-extension/dist/icon.svg +6 -0
- package/packages/chrome-attach-extension/dist/manifest.json +36 -0
- package/packages/chrome-attach-extension/dist/options.html +312 -0
- package/packages/chrome-attach-extension/dist/options.js +593 -0
- package/packages/chrome-attach-extension/dist/popup.html +93 -0
- package/packages/chrome-attach-extension/dist/popup.js +79 -0
- package/packages/chrome-attach-extension/dist/recorder-content.js +83 -0
- package/packages/chrome-attach-extension/dist/recorder-page.js +266 -0
- package/packages/firefox-attach-extension/README.md +13 -0
- package/packages/firefox-attach-extension/dist/background.js +1242 -0
- package/packages/firefox-attach-extension/dist/icon.svg +6 -0
- package/packages/firefox-attach-extension/dist/manifest.json +56 -0
- package/packages/firefox-attach-extension/dist/options.html +312 -0
- package/packages/firefox-attach-extension/dist/options.js +527 -0
- package/packages/firefox-attach-extension/dist/popup.html +93 -0
- package/packages/firefox-attach-extension/dist/popup.js +64 -0
- package/packages/firefox-attach-extension/dist/recorder-content.js +77 -0
- package/packages/firefox-attach-extension/dist/recorder-page.js +302 -0
package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FirefoxAttachServer = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const ws_1 = __importDefault(require("ws"));
|
|
9
|
+
const browserRecordingBundle_1 = require("./browserRecordingBundle");
|
|
10
|
+
const firefoxAttachRegistry_1 = require("./firefoxAttachRegistry");
|
|
11
|
+
const wsLib = ws_1.default;
|
|
12
|
+
const WebSocketServer = wsLib.WebSocketServer;
|
|
13
|
+
const WS_OPEN = wsLib.OPEN;
|
|
14
|
+
class FirefoxAttachServer {
|
|
15
|
+
config;
|
|
16
|
+
logger;
|
|
17
|
+
sessionStore;
|
|
18
|
+
maintenanceStore;
|
|
19
|
+
wsServer = null;
|
|
20
|
+
registry = new firefoxAttachRegistry_1.FirefoxAttachRegistry();
|
|
21
|
+
recordingWriter;
|
|
22
|
+
sockets = new Set();
|
|
23
|
+
socketsByInstanceId = new Map();
|
|
24
|
+
pendingTabActions = new Map();
|
|
25
|
+
pendingRecordingControls = new Map();
|
|
26
|
+
activeRecordingsById = new Map();
|
|
27
|
+
constructor(config, logger, sessionStore, maintenanceStore) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
this.sessionStore = sessionStore;
|
|
31
|
+
this.maintenanceStore = maintenanceStore;
|
|
32
|
+
this.recordingWriter = new browserRecordingBundle_1.BrowserRecordingBundleWriter(config, logger);
|
|
33
|
+
}
|
|
34
|
+
async start() {
|
|
35
|
+
if (!this.config.browser.attach.enabled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (this.wsServer) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.wsServer = new WebSocketServer({
|
|
42
|
+
host: this.config.browser.attach.host,
|
|
43
|
+
port: this.config.browser.attach.port,
|
|
44
|
+
path: this.config.browser.attach.path,
|
|
45
|
+
});
|
|
46
|
+
this.wsServer.on("connection", (socket) => {
|
|
47
|
+
const state = { socket };
|
|
48
|
+
this.sockets.add(state);
|
|
49
|
+
socket.on("message", (payload) => {
|
|
50
|
+
void this.handleMessage(state, payload);
|
|
51
|
+
});
|
|
52
|
+
socket.on("close", () => {
|
|
53
|
+
if (state.instanceId) {
|
|
54
|
+
void this.handleInstanceDisconnect(state.instanceId);
|
|
55
|
+
this.registry.remove(state.instanceId);
|
|
56
|
+
this.socketsByInstanceId.delete(state.instanceId);
|
|
57
|
+
this.logger.info("Firefox attach instance disconnected", {
|
|
58
|
+
instanceId: state.instanceId,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
this.sockets.delete(state);
|
|
62
|
+
});
|
|
63
|
+
socket.on("error", (error) => {
|
|
64
|
+
this.logger.debug("Firefox attach socket error", {
|
|
65
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
this.logger.info("Firefox attach WebSocket server started", {
|
|
70
|
+
host: this.config.browser.attach.host,
|
|
71
|
+
port: this.config.browser.attach.port,
|
|
72
|
+
path: this.config.browser.attach.path,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async stop() {
|
|
76
|
+
if (!this.wsServer) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const state of this.sockets) {
|
|
80
|
+
try {
|
|
81
|
+
state.socket.close(1001, "server shutdown");
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.sockets.clear();
|
|
88
|
+
const wsServer = this.wsServer;
|
|
89
|
+
this.wsServer = null;
|
|
90
|
+
await new Promise((resolve, reject) => {
|
|
91
|
+
wsServer.close((error) => {
|
|
92
|
+
if (error) {
|
|
93
|
+
reject(error);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
this.logger.info("Firefox attach WebSocket server stopped");
|
|
100
|
+
}
|
|
101
|
+
listInstances() {
|
|
102
|
+
return this.registry.listInstances();
|
|
103
|
+
}
|
|
104
|
+
async getRecordingStatus(sessionId) {
|
|
105
|
+
return this.maintenanceStore.getBrowserRecording(sessionId);
|
|
106
|
+
}
|
|
107
|
+
async startRecording(input) {
|
|
108
|
+
const existing = await this.maintenanceStore.getBrowserRecording(input.sessionId);
|
|
109
|
+
if (existing?.status === "recording") {
|
|
110
|
+
if (existing.instanceId !== input.instanceId) {
|
|
111
|
+
throw new Error("Recording is already active in another browser instance.");
|
|
112
|
+
}
|
|
113
|
+
return existing;
|
|
114
|
+
}
|
|
115
|
+
const session = await this.sessionStore.getSession(input.sessionId);
|
|
116
|
+
const resolvedSession = session ||
|
|
117
|
+
{
|
|
118
|
+
sessionId: input.sessionId,
|
|
119
|
+
label: this.config.project.sessionLabel?.trim() || input.sessionId,
|
|
120
|
+
cwd: process.cwd(),
|
|
121
|
+
updatedAt: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
if (!resolvedSession.cwd?.trim()) {
|
|
124
|
+
throw new Error("Workspace cwd is not registered for this console.");
|
|
125
|
+
}
|
|
126
|
+
if (!session) {
|
|
127
|
+
await this.sessionStore.setSession({
|
|
128
|
+
sessionId: resolvedSession.sessionId,
|
|
129
|
+
...(resolvedSession.label ? { label: resolvedSession.label } : {}),
|
|
130
|
+
cwd: resolvedSession.cwd,
|
|
131
|
+
updatedAt: resolvedSession.updatedAt,
|
|
132
|
+
});
|
|
133
|
+
this.logger.info("Firefox attach recording synthesized missing session context", {
|
|
134
|
+
sessionId: input.sessionId,
|
|
135
|
+
cwd: resolvedSession.cwd,
|
|
136
|
+
label: resolvedSession.label,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const recordingId = (0, node_crypto_1.randomUUID)();
|
|
140
|
+
const state = await this.recordingWriter.create({
|
|
141
|
+
session: resolvedSession,
|
|
142
|
+
sessionId: input.sessionId,
|
|
143
|
+
instanceId: input.instanceId,
|
|
144
|
+
tabId: input.tabId,
|
|
145
|
+
tabTitle: input.tabTitle,
|
|
146
|
+
...(input.tabUrl ? { tabUrl: input.tabUrl } : {}),
|
|
147
|
+
recordingId,
|
|
148
|
+
});
|
|
149
|
+
this.activeRecordingsById.set(recordingId, state);
|
|
150
|
+
await this.maintenanceStore.setBrowserRecording(state.record);
|
|
151
|
+
await this.broadcastRecordingState(state.record);
|
|
152
|
+
try {
|
|
153
|
+
await this.invokeRecordingControl({
|
|
154
|
+
instanceId: input.instanceId,
|
|
155
|
+
tabId: input.tabId,
|
|
156
|
+
recordingId,
|
|
157
|
+
mode: "start",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
this.activeRecordingsById.delete(recordingId);
|
|
162
|
+
await this.maintenanceStore.clearBrowserRecording(input.sessionId);
|
|
163
|
+
await this.broadcastRecordingState(null);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
return state.record;
|
|
167
|
+
}
|
|
168
|
+
async stopRecording(input) {
|
|
169
|
+
const existing = await this.maintenanceStore.getBrowserRecording(input.sessionId);
|
|
170
|
+
if (!existing) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const activeState = this.activeRecordingsById.get(existing.recordingId);
|
|
174
|
+
if (activeState) {
|
|
175
|
+
try {
|
|
176
|
+
await this.invokeRecordingControl({
|
|
177
|
+
instanceId: activeState.record.instanceId,
|
|
178
|
+
tabId: activeState.record.tabId,
|
|
179
|
+
recordingId: activeState.record.recordingId,
|
|
180
|
+
mode: "stop",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Local writer still finalizes the bundle.
|
|
185
|
+
}
|
|
186
|
+
await this.recordingWriter.appendEvent(activeState, {
|
|
187
|
+
kind: "session_stopped",
|
|
188
|
+
status: "stopped",
|
|
189
|
+
});
|
|
190
|
+
this.activeRecordingsById.delete(existing.recordingId);
|
|
191
|
+
await this.maintenanceStore.setBrowserRecording(activeState.record);
|
|
192
|
+
await this.broadcastRecordingState(activeState.record);
|
|
193
|
+
return activeState.record;
|
|
194
|
+
}
|
|
195
|
+
if (existing.status !== "stopped") {
|
|
196
|
+
const stopped = {
|
|
197
|
+
...existing,
|
|
198
|
+
status: "stopped",
|
|
199
|
+
stoppedAt: new Date().toISOString(),
|
|
200
|
+
};
|
|
201
|
+
await this.maintenanceStore.setBrowserRecording(stopped);
|
|
202
|
+
await this.broadcastRecordingState(stopped);
|
|
203
|
+
return stopped;
|
|
204
|
+
}
|
|
205
|
+
return existing;
|
|
206
|
+
}
|
|
207
|
+
async invokeTabAction(input) {
|
|
208
|
+
const socketState = this.socketsByInstanceId.get(input.instanceId);
|
|
209
|
+
if (!socketState) {
|
|
210
|
+
throw new Error(`Attached browser instance '${input.instanceId}' is not connected.`);
|
|
211
|
+
}
|
|
212
|
+
const requestId = `tab-action-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
213
|
+
const result = await new Promise((resolve, reject) => {
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
this.pendingTabActions.delete(requestId);
|
|
216
|
+
reject(new Error(`Attached browser action '${input.action}' timed out for instance '${input.instanceId}'.`));
|
|
217
|
+
}, this.config.browser.timeoutMs);
|
|
218
|
+
this.pendingTabActions.set(requestId, {
|
|
219
|
+
resolve,
|
|
220
|
+
reject,
|
|
221
|
+
timer,
|
|
222
|
+
});
|
|
223
|
+
this.sendJson(socketState.socket, {
|
|
224
|
+
type: "tab_action",
|
|
225
|
+
request_id: requestId,
|
|
226
|
+
tab_id: input.tabId,
|
|
227
|
+
action: input.action,
|
|
228
|
+
...(input.payload ? { payload: input.payload } : {}),
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
if (!result.ok) {
|
|
232
|
+
throw new Error(result.error ||
|
|
233
|
+
`Attached browser action '${input.action}' failed for instance '${input.instanceId}'.`);
|
|
234
|
+
}
|
|
235
|
+
return result.result;
|
|
236
|
+
}
|
|
237
|
+
sendJson(socket, payload) {
|
|
238
|
+
if (socket.readyState !== WS_OPEN) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
socket.send(JSON.stringify(payload));
|
|
242
|
+
}
|
|
243
|
+
async handleMessage(state, payload) {
|
|
244
|
+
const raw = typeof payload === "string"
|
|
245
|
+
? payload
|
|
246
|
+
: Buffer.isBuffer(payload)
|
|
247
|
+
? payload.toString("utf8")
|
|
248
|
+
: String(payload ?? "");
|
|
249
|
+
let message;
|
|
250
|
+
try {
|
|
251
|
+
message = JSON.parse(raw);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
this.logger.debug("Firefox attach message ignored because it is not valid JSON");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
switch (message.type) {
|
|
258
|
+
case "hello":
|
|
259
|
+
await this.handleHello(state, message);
|
|
260
|
+
return;
|
|
261
|
+
case "heartbeat":
|
|
262
|
+
if (state.instanceId) {
|
|
263
|
+
this.registry.touch(state.instanceId);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
case "list_tabs_result":
|
|
267
|
+
if (state.instanceId) {
|
|
268
|
+
this.registry.setTabs(state.instanceId, message.tabs);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
case "get_active_tab_result":
|
|
272
|
+
if (state.instanceId) {
|
|
273
|
+
this.registry.setActiveTab(state.instanceId, message.tab);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
case "active_tab_changed":
|
|
277
|
+
if (state.instanceId) {
|
|
278
|
+
this.registry.setActiveTab(state.instanceId, message.tab);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
case "tab_updated":
|
|
282
|
+
if (state.instanceId) {
|
|
283
|
+
this.registry.updateTab(state.instanceId, message.tab);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
case "attach_tab_selected":
|
|
287
|
+
if (state.instanceId) {
|
|
288
|
+
this.registry.updateTab(state.instanceId, {
|
|
289
|
+
...message.tab,
|
|
290
|
+
active: true,
|
|
291
|
+
});
|
|
292
|
+
await this.persistSelectedTab(state.instanceId, message.tab);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
case "tab_action_result":
|
|
296
|
+
this.resolvePendingTabAction(message);
|
|
297
|
+
return;
|
|
298
|
+
case "recording_control_result":
|
|
299
|
+
this.resolvePendingRecordingControl(message);
|
|
300
|
+
return;
|
|
301
|
+
case "recording_event":
|
|
302
|
+
if (state.instanceId) {
|
|
303
|
+
await this.handleRecordingEvent(state.instanceId, message.recording_id, message.tab_id, message.event);
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
case "recording_manual_start":
|
|
307
|
+
case "recording_manual_stop":
|
|
308
|
+
case "recording_manual_status":
|
|
309
|
+
if (state.instanceId) {
|
|
310
|
+
await this.handleManualRecordingRequest(state.socket, state.instanceId, message);
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
default:
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async handleHello(state, message) {
|
|
318
|
+
state.instanceId = message.instance_id;
|
|
319
|
+
this.socketsByInstanceId.set(message.instance_id, state);
|
|
320
|
+
const instance = this.registry.setConnected({
|
|
321
|
+
instanceId: message.instance_id,
|
|
322
|
+
extensionVersion: message.extension_version,
|
|
323
|
+
...(message.profile_name ? { profileName: message.profile_name } : {}),
|
|
324
|
+
capabilities: ["tabs", "active_tab", "recording"],
|
|
325
|
+
});
|
|
326
|
+
this.sendJson(state.socket, {
|
|
327
|
+
type: "hello_ack",
|
|
328
|
+
ok: true,
|
|
329
|
+
instance_id: message.instance_id,
|
|
330
|
+
capabilities: instance.capabilities,
|
|
331
|
+
...(this.config.project.sessionId
|
|
332
|
+
? { session_id: this.config.project.sessionId }
|
|
333
|
+
: {}),
|
|
334
|
+
...(this.config.project.sessionLabel
|
|
335
|
+
? { session_label: this.config.project.sessionLabel }
|
|
336
|
+
: {}),
|
|
337
|
+
});
|
|
338
|
+
this.logger.info("Firefox attach instance connected", {
|
|
339
|
+
instanceId: message.instance_id,
|
|
340
|
+
extensionVersion: message.extension_version,
|
|
341
|
+
profileName: message.profile_name,
|
|
342
|
+
});
|
|
343
|
+
this.sendJson(state.socket, {
|
|
344
|
+
type: "list_tabs",
|
|
345
|
+
request_id: "bootstrap-list-tabs",
|
|
346
|
+
});
|
|
347
|
+
this.sendJson(state.socket, {
|
|
348
|
+
type: "get_active_tab",
|
|
349
|
+
request_id: "bootstrap-active-tab",
|
|
350
|
+
});
|
|
351
|
+
await this.sendCurrentRecordingState(state.socket);
|
|
352
|
+
}
|
|
353
|
+
async persistSelectedTab(instanceId, tab) {
|
|
354
|
+
const sessionId = this.config.project.sessionId?.trim();
|
|
355
|
+
if (!sessionId) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
await this.maintenanceStore.setBrowserAttachment({
|
|
359
|
+
sessionId,
|
|
360
|
+
backend: "firefox-attached",
|
|
361
|
+
instanceId,
|
|
362
|
+
tabId: tab.tab_id,
|
|
363
|
+
attachedAt: new Date().toISOString(),
|
|
364
|
+
...(tab.title ? { title: tab.title } : {}),
|
|
365
|
+
...(tab.url ? { url: tab.url } : {}),
|
|
366
|
+
});
|
|
367
|
+
this.logger.info("Firefox attached tab selected", {
|
|
368
|
+
sessionId,
|
|
369
|
+
instanceId,
|
|
370
|
+
tabId: tab.tab_id,
|
|
371
|
+
title: tab.title,
|
|
372
|
+
url: tab.url,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
resolvePendingTabAction(message) {
|
|
376
|
+
const pending = this.pendingTabActions.get(message.request_id);
|
|
377
|
+
if (!pending) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
clearTimeout(pending.timer);
|
|
381
|
+
this.pendingTabActions.delete(message.request_id);
|
|
382
|
+
pending.resolve(message);
|
|
383
|
+
}
|
|
384
|
+
resolvePendingRecordingControl(message) {
|
|
385
|
+
const pending = this.pendingRecordingControls.get(message.request_id);
|
|
386
|
+
if (!pending) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
clearTimeout(pending.timer);
|
|
390
|
+
this.pendingRecordingControls.delete(message.request_id);
|
|
391
|
+
pending.resolve(message);
|
|
392
|
+
}
|
|
393
|
+
async invokeRecordingControl(input) {
|
|
394
|
+
const socketState = this.socketsByInstanceId.get(input.instanceId);
|
|
395
|
+
if (!socketState) {
|
|
396
|
+
throw new Error(`Attached browser instance '${input.instanceId}' is not connected.`);
|
|
397
|
+
}
|
|
398
|
+
const requestId = `recording-${input.mode}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
399
|
+
const result = await new Promise((resolve, reject) => {
|
|
400
|
+
const timer = setTimeout(() => {
|
|
401
|
+
this.pendingRecordingControls.delete(requestId);
|
|
402
|
+
reject(new Error(`Attached browser recording '${input.mode}' timed out for instance '${input.instanceId}'.`));
|
|
403
|
+
}, this.config.browser.timeoutMs);
|
|
404
|
+
this.pendingRecordingControls.set(requestId, {
|
|
405
|
+
resolve,
|
|
406
|
+
reject,
|
|
407
|
+
timer,
|
|
408
|
+
});
|
|
409
|
+
this.sendJson(socketState.socket, input.mode === "start"
|
|
410
|
+
? {
|
|
411
|
+
type: "recording_start",
|
|
412
|
+
request_id: requestId,
|
|
413
|
+
recording_id: input.recordingId,
|
|
414
|
+
tab_id: input.tabId,
|
|
415
|
+
}
|
|
416
|
+
: {
|
|
417
|
+
type: "recording_stop",
|
|
418
|
+
request_id: requestId,
|
|
419
|
+
recording_id: input.recordingId,
|
|
420
|
+
tab_id: input.tabId,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
if (!result.ok) {
|
|
424
|
+
throw new Error(result.error ||
|
|
425
|
+
`Attached browser recording '${input.mode}' failed for instance '${input.instanceId}'.`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async handleRecordingEvent(instanceId, recordingId, tabId, event) {
|
|
429
|
+
const state = this.activeRecordingsById.get(recordingId);
|
|
430
|
+
if (!state) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (state.record.instanceId !== instanceId || state.record.tabId !== tabId) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
await this.recordingWriter.appendEvent(state, event);
|
|
437
|
+
await this.maintenanceStore.setBrowserRecording(state.record);
|
|
438
|
+
}
|
|
439
|
+
async handleInstanceDisconnect(instanceId) {
|
|
440
|
+
const activeStates = Array.from(this.activeRecordingsById.values()).filter((state) => state.record.instanceId === instanceId);
|
|
441
|
+
if (activeStates.length === 0) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
for (const state of activeStates) {
|
|
445
|
+
await this.recordingWriter.appendEvent(state, {
|
|
446
|
+
kind: "session_stopped",
|
|
447
|
+
status: "instance_disconnected",
|
|
448
|
+
});
|
|
449
|
+
this.activeRecordingsById.delete(state.record.recordingId);
|
|
450
|
+
await this.maintenanceStore.setBrowserRecording(state.record);
|
|
451
|
+
await this.broadcastRecordingState(state.record);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
buildRecordingStateMessage(record) {
|
|
455
|
+
return {
|
|
456
|
+
type: "recording_state",
|
|
457
|
+
active: record?.status === "recording",
|
|
458
|
+
...(record ? { recording: this.mapManualRecordingResult(record) } : {}),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
async sendCurrentRecordingState(socket) {
|
|
462
|
+
const sessionId = this.config.project.sessionId?.trim();
|
|
463
|
+
const record = sessionId
|
|
464
|
+
? await this.maintenanceStore.getBrowserRecording(sessionId)
|
|
465
|
+
: null;
|
|
466
|
+
this.sendJson(socket, this.buildRecordingStateMessage(record));
|
|
467
|
+
}
|
|
468
|
+
async broadcastRecordingState(record) {
|
|
469
|
+
const payload = this.buildRecordingStateMessage(record);
|
|
470
|
+
for (const state of this.sockets) {
|
|
471
|
+
this.sendJson(state.socket, payload);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
mapManualRecordingResult(record) {
|
|
475
|
+
if (!record) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
recording_id: record.recordingId,
|
|
480
|
+
instance_id: record.instanceId,
|
|
481
|
+
tab_id: record.tabId,
|
|
482
|
+
...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
|
|
483
|
+
...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
|
|
484
|
+
bundle_dir_name: record.bundleDirName,
|
|
485
|
+
bundle_relative_path: record.bundleRelativePath,
|
|
486
|
+
bundle_path: record.bundlePath,
|
|
487
|
+
started_at: record.startedAt,
|
|
488
|
+
...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
|
|
489
|
+
status: record.status,
|
|
490
|
+
event_count: record.eventCount,
|
|
491
|
+
...(record.lastEventAt ? { last_event_at: record.lastEventAt } : {}),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
async handleManualRecordingRequest(socket, instanceId, message) {
|
|
495
|
+
const sessionId = this.config.project.sessionId?.trim();
|
|
496
|
+
if (!sessionId) {
|
|
497
|
+
this.sendJson(socket, {
|
|
498
|
+
type: "recording_manual_result",
|
|
499
|
+
request_id: message.request_id,
|
|
500
|
+
ok: false,
|
|
501
|
+
active: false,
|
|
502
|
+
error: "Session id is not configured for this attach server.",
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
if (message.type === "recording_manual_status") {
|
|
508
|
+
const record = await this.getRecordingStatus(sessionId);
|
|
509
|
+
this.sendJson(socket, {
|
|
510
|
+
type: "recording_manual_result",
|
|
511
|
+
request_id: message.request_id,
|
|
512
|
+
ok: true,
|
|
513
|
+
active: record?.status === "recording",
|
|
514
|
+
...(record ? { recording: this.mapManualRecordingResult(record) } : {}),
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (message.type === "recording_manual_stop") {
|
|
519
|
+
const record = await this.stopRecording({ sessionId });
|
|
520
|
+
this.sendJson(socket, {
|
|
521
|
+
type: "recording_manual_result",
|
|
522
|
+
request_id: message.request_id,
|
|
523
|
+
ok: true,
|
|
524
|
+
active: record?.status === "recording",
|
|
525
|
+
...(record ? { recording: this.mapManualRecordingResult(record) } : {}),
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const tab = message.tab;
|
|
530
|
+
if (!tab) {
|
|
531
|
+
throw new Error("Selected tab metadata is required to start recording.");
|
|
532
|
+
}
|
|
533
|
+
const record = await this.startRecording({
|
|
534
|
+
sessionId,
|
|
535
|
+
instanceId,
|
|
536
|
+
tabId: tab.tab_id,
|
|
537
|
+
tabTitle: tab.title || "attached-tab",
|
|
538
|
+
...(tab.url ? { tabUrl: tab.url } : {}),
|
|
539
|
+
});
|
|
540
|
+
this.sendJson(socket, {
|
|
541
|
+
type: "recording_manual_result",
|
|
542
|
+
request_id: message.request_id,
|
|
543
|
+
ok: true,
|
|
544
|
+
active: true,
|
|
545
|
+
recording: this.mapManualRecordingResult(record),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
this.sendJson(socket, {
|
|
550
|
+
type: "recording_manual_result",
|
|
551
|
+
request_id: message.request_id,
|
|
552
|
+
ok: false,
|
|
553
|
+
active: false,
|
|
554
|
+
error: error instanceof Error ? error.message : String(error),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
exports.FirefoxAttachServer = FirefoxAttachServer;
|
|
@@ -44,6 +44,12 @@ function adminAuthorizedMatchPattern() {
|
|
|
44
44
|
function gatewayClientUuidKey() {
|
|
45
45
|
return `${KEY_PREFIX}:gateway:client-uuid`;
|
|
46
46
|
}
|
|
47
|
+
function browserAttachmentKey(sessionId) {
|
|
48
|
+
return `${KEY_PREFIX}:browser-attachment:${sessionId}`;
|
|
49
|
+
}
|
|
50
|
+
function browserRecordingKey(sessionId) {
|
|
51
|
+
return `${KEY_PREFIX}:browser-recording:${sessionId}`;
|
|
52
|
+
}
|
|
47
53
|
function outgoingDeliveryNoticeKey(deliveryUuid) {
|
|
48
54
|
return `${KEY_PREFIX}:gateway:outgoing-delivery:${deliveryUuid}`;
|
|
49
55
|
}
|
|
@@ -103,6 +109,8 @@ class RedisStateStore {
|
|
|
103
109
|
await this.clearXchangeFileMetas(sessionId);
|
|
104
110
|
await this.clearProjectMenuViewStates(sessionId);
|
|
105
111
|
await this.clearOutgoingDeliveryNoticesForSession(sessionId);
|
|
112
|
+
await this.clearBrowserAttachment(sessionId);
|
|
113
|
+
await this.clearBrowserRecording(sessionId);
|
|
106
114
|
await this.sessionAdapter.delete(sessionKey(sessionId));
|
|
107
115
|
}
|
|
108
116
|
async createPairCode(record, ttlSeconds) {
|
|
@@ -340,6 +348,26 @@ class RedisStateStore {
|
|
|
340
348
|
async setGatewayClientUuid(clientUuid) {
|
|
341
349
|
await this.redis.set(gatewayClientUuidKey(), clientUuid);
|
|
342
350
|
}
|
|
351
|
+
async getBrowserAttachment(sessionId) {
|
|
352
|
+
const raw = await this.redis.get(browserAttachmentKey(sessionId));
|
|
353
|
+
return parseJson(raw);
|
|
354
|
+
}
|
|
355
|
+
async setBrowserAttachment(record) {
|
|
356
|
+
await this.redis.set(browserAttachmentKey(record.sessionId), JSON.stringify(record));
|
|
357
|
+
}
|
|
358
|
+
async clearBrowserAttachment(sessionId) {
|
|
359
|
+
await this.redis.del(browserAttachmentKey(sessionId));
|
|
360
|
+
}
|
|
361
|
+
async getBrowserRecording(sessionId) {
|
|
362
|
+
const raw = await this.redis.get(browserRecordingKey(sessionId));
|
|
363
|
+
return parseJson(raw);
|
|
364
|
+
}
|
|
365
|
+
async setBrowserRecording(record) {
|
|
366
|
+
await this.redis.set(browserRecordingKey(record.sessionId), JSON.stringify(record));
|
|
367
|
+
}
|
|
368
|
+
async clearBrowserRecording(sessionId) {
|
|
369
|
+
await this.redis.del(browserRecordingKey(sessionId));
|
|
370
|
+
}
|
|
343
371
|
async setProjectMenuViewState(state) {
|
|
344
372
|
await this.redis.set(projectMenuViewStateKey(state.projectUuid, state.sessionId), JSON.stringify(state));
|
|
345
373
|
}
|