@deadragdoll/tellymcp 0.0.11 → 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 +48 -0
- package/README.md +47 -0
- package/TOOLS.md +232 -3
- package/VERSION.md +2 -2
- package/dist/cli.js +109 -1
- package/dist/services/features/telegram-mcp/browser.service.js +38 -1
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +10 -0
- 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 +151 -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/browserOpenTool.js +1 -1
- 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 +543 -9
- 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/features/distributed-gateway/model/gatewayHttpService.js +48 -0
- package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +56 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +37 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +13 -7
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConstructorWiring.js +9 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMenuShell.js +3 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportPayloadState.js +7 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalActions.js +231 -34
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalRuntime.js +61 -6
- package/dist/services/features/telegram-mcp/src/shared/lib/terminalPromptDetection.js +200 -28
- 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;
|
|
@@ -676,6 +676,54 @@ class GatewayHttpService {
|
|
|
676
676
|
return true;
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
|
+
if (pathname === "/gateway/live/action") {
|
|
680
|
+
if (req.method !== "POST") {
|
|
681
|
+
writeText(res, 405, "Method not allowed");
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const body = (await this.readJsonBody(req));
|
|
686
|
+
const sessionId = typeof body.session_id === "string" ? body.session_id.trim() : "";
|
|
687
|
+
const action = typeof body.action === "string" ? body.action.trim() : "";
|
|
688
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
689
|
+
const relayTarget = (0, relay_1.parseLiveRelaySessionId)(sessionId);
|
|
690
|
+
if (!relayTarget) {
|
|
691
|
+
writeJson(res, 400, {
|
|
692
|
+
error: "relay session_id is required",
|
|
693
|
+
});
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
if (!["enter", "escape", "text"].includes(action)) {
|
|
697
|
+
writeJson(res, 400, {
|
|
698
|
+
error: "action must be one of: enter, escape, text",
|
|
699
|
+
});
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
if (action === "text" && (!text || text.length > 16)) {
|
|
703
|
+
writeJson(res, 400, {
|
|
704
|
+
error: "text payload is required and must be <= 16 characters",
|
|
705
|
+
});
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
await this.requestLiveRelayAction({
|
|
709
|
+
clientUuid: relayTarget.clientUuid,
|
|
710
|
+
localSessionId: relayTarget.localSessionId,
|
|
711
|
+
action: action,
|
|
712
|
+
...(action === "text" ? { text } : {}),
|
|
713
|
+
});
|
|
714
|
+
writeJson(res, 200, { ok: true });
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
console.error("Gateway live action request failed", {
|
|
719
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
720
|
+
});
|
|
721
|
+
writeJson(res, 400, {
|
|
722
|
+
error: error instanceof Error ? error.message : String(error),
|
|
723
|
+
});
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
679
727
|
if (pathname === "/gateway/client/register") {
|
|
680
728
|
if (req.method !== "POST") {
|
|
681
729
|
writeText(res, 405, "Method not allowed");
|
|
@@ -396,11 +396,15 @@ exports.enMenu = {
|
|
|
396
396
|
unavailable_target: "terminal target: {{terminalTarget}}",
|
|
397
397
|
unavailable_reason: "This usually means the terminal runtime is not running or is unreachable for the current target.",
|
|
398
398
|
unavailable_action: "Restart the terminal runtime for this console, or update/remove the terminal target for this session.",
|
|
399
|
-
prompt_detected_title: "🛎
|
|
399
|
+
prompt_detected_title: "🛎 Possible blocker in {{sessionName}}",
|
|
400
400
|
prompt_detected_score: "Detection score: {{score}}",
|
|
401
401
|
prompt_detected_target: "terminal target: {{terminalTarget}}",
|
|
402
402
|
prompt_detected_hint: "Open Live or answer in the terminal if this prompt really needs you.",
|
|
403
403
|
prompt_detected_excerpt: "Recent prompt lines:",
|
|
404
|
+
prompt_action_sent: "Sent {{action}} to terminal.",
|
|
405
|
+
prompt_action_stale: "This blocker action is no longer available.",
|
|
406
|
+
prompt_action_failed: "Failed to send the action to terminal.",
|
|
407
|
+
prompt_action_invalid: "Invalid blocker action.",
|
|
404
408
|
},
|
|
405
409
|
},
|
|
406
410
|
admin: {
|
|
@@ -396,11 +396,15 @@ exports.ruMenu = {
|
|
|
396
396
|
unavailable_target: "terminal target: {{terminalTarget}}",
|
|
397
397
|
unavailable_reason: "Обычно это значит, что терминальный runtime не запущен или недоступен для текущего target.",
|
|
398
398
|
unavailable_action: "Перезапусти терминальный runtime для этой консоли или обнови/сними terminal target для этой сессии.",
|
|
399
|
-
prompt_detected_title: "🛎
|
|
399
|
+
prompt_detected_title: "🛎 Возможный блокер в {{sessionName}}",
|
|
400
400
|
prompt_detected_score: "Сила срабатывания: {{score}}",
|
|
401
401
|
prompt_detected_target: "terminal target: {{terminalTarget}}",
|
|
402
402
|
prompt_detected_hint: "Открой Live или ответь в терминале, если этот prompt действительно требует тебя.",
|
|
403
403
|
prompt_detected_excerpt: "Последние строки prompt:",
|
|
404
|
+
prompt_action_sent: "Отправил {{action}} в терминал.",
|
|
405
|
+
prompt_action_stale: "Это действие для блокера уже недоступно.",
|
|
406
|
+
prompt_action_failed: "Не удалось отправить действие в терминал.",
|
|
407
|
+
prompt_action_invalid: "Некорректное действие блокера.",
|
|
404
408
|
},
|
|
405
409
|
},
|
|
406
410
|
admin: {
|