@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
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.BrowserService = void 0;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
7
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
9
|
const gatewayClientAccess_1 = require("../../distributed-client/model/gatewayClientAccess");
|
|
9
10
|
function pushBounded(list, entry, max) {
|
|
@@ -47,6 +48,8 @@ function formatConsoleLocation(message) {
|
|
|
47
48
|
function escapeCssAttributeValue(value) {
|
|
48
49
|
return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
|
|
49
50
|
}
|
|
51
|
+
const DEFAULT_BROWSER_VIEWPORT_WIDTH = 1720;
|
|
52
|
+
const DEFAULT_BROWSER_VIEWPORT_HEIGHT = 980;
|
|
50
53
|
class BrowserService {
|
|
51
54
|
config;
|
|
52
55
|
sessionStore;
|
|
@@ -58,10 +61,11 @@ class BrowserService {
|
|
|
58
61
|
logger;
|
|
59
62
|
projectIdentityResolver;
|
|
60
63
|
remoteConsoleInvoker;
|
|
64
|
+
firefoxAttachRegistry;
|
|
61
65
|
playwrightModulePromise;
|
|
62
66
|
browserPromise;
|
|
63
67
|
sessionStates = new Map();
|
|
64
|
-
constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker) {
|
|
68
|
+
constructor(config, sessionStore, maintenanceStore, bindingStore, xchangeFileMetaStore, objectStore, telegramTransport, logger, projectIdentityResolver, remoteConsoleInvoker, firefoxAttachRegistry) {
|
|
65
69
|
this.config = config;
|
|
66
70
|
this.sessionStore = sessionStore;
|
|
67
71
|
this.maintenanceStore = maintenanceStore;
|
|
@@ -72,6 +76,173 @@ class BrowserService {
|
|
|
72
76
|
this.logger = logger;
|
|
73
77
|
this.projectIdentityResolver = projectIdentityResolver;
|
|
74
78
|
this.remoteConsoleInvoker = remoteConsoleInvoker;
|
|
79
|
+
this.firefoxAttachRegistry = firefoxAttachRegistry;
|
|
80
|
+
}
|
|
81
|
+
async listAttachedInstances(input) {
|
|
82
|
+
const resolvedSessionId = await this.resolveOptionalSessionIdForRemote(input);
|
|
83
|
+
if (resolvedSessionId) {
|
|
84
|
+
const remote = await this.invokeRemote(resolvedSessionId, "telegramMcp.browser.listAttachedInstancesRemote", {
|
|
85
|
+
...input,
|
|
86
|
+
session_id: resolvedSessionId,
|
|
87
|
+
});
|
|
88
|
+
if (remote) {
|
|
89
|
+
return remote;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const instances = this.listFirefoxAttachInstances();
|
|
93
|
+
return {
|
|
94
|
+
...(resolvedSessionId ? { session_id: resolvedSessionId } : {}),
|
|
95
|
+
total: instances.length,
|
|
96
|
+
instances: instances.map((instance) => ({
|
|
97
|
+
instance_id: instance.instanceId,
|
|
98
|
+
browser: instance.browser,
|
|
99
|
+
extension_version: instance.extensionVersion,
|
|
100
|
+
...(instance.profileName ? { profile_name: instance.profileName } : {}),
|
|
101
|
+
connected_at: instance.connectedAt,
|
|
102
|
+
last_seen_at: instance.lastSeenAt,
|
|
103
|
+
capabilities: [...instance.capabilities],
|
|
104
|
+
tab_count: instance.tabs.length,
|
|
105
|
+
...(instance.activeTab
|
|
106
|
+
? {
|
|
107
|
+
active_tab: {
|
|
108
|
+
tab_id: instance.activeTab.tab_id,
|
|
109
|
+
...(typeof instance.activeTab.window_id === "number"
|
|
110
|
+
? { window_id: instance.activeTab.window_id }
|
|
111
|
+
: {}),
|
|
112
|
+
active: instance.activeTab.active,
|
|
113
|
+
title: instance.activeTab.title,
|
|
114
|
+
url: instance.activeTab.url,
|
|
115
|
+
...(instance.activeTab.status
|
|
116
|
+
? { status: instance.activeTab.status }
|
|
117
|
+
: {}),
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
121
|
+
})),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async listTabs(input) {
|
|
125
|
+
const resolvedSessionId = await this.resolveOptionalSessionIdForRemote(input);
|
|
126
|
+
if (resolvedSessionId) {
|
|
127
|
+
const remote = await this.invokeRemote(resolvedSessionId, "telegramMcp.browser.listTabsRemote", {
|
|
128
|
+
...input,
|
|
129
|
+
session_id: resolvedSessionId,
|
|
130
|
+
});
|
|
131
|
+
if (remote) {
|
|
132
|
+
return remote;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const instances = this.listFirefoxAttachInstances();
|
|
136
|
+
const instance = input.instance_id?.trim()
|
|
137
|
+
? instances.find((item) => item.instanceId === input.instance_id?.trim())
|
|
138
|
+
: instances.length === 1
|
|
139
|
+
? instances[0]
|
|
140
|
+
: undefined;
|
|
141
|
+
if (!instance) {
|
|
142
|
+
if (input.instance_id?.trim()) {
|
|
143
|
+
throw new Error(`Attached browser instance '${input.instance_id.trim()}' was not found.`);
|
|
144
|
+
}
|
|
145
|
+
throw new Error("Attached browser instance is ambiguous. Pass instance_id or keep exactly one connected browser instance.");
|
|
146
|
+
}
|
|
147
|
+
const selectedAttachment = resolvedSessionId
|
|
148
|
+
? await this.getAttachedBrowserAttachment(resolvedSessionId)
|
|
149
|
+
: null;
|
|
150
|
+
return {
|
|
151
|
+
...(resolvedSessionId ? { session_id: resolvedSessionId } : {}),
|
|
152
|
+
instance_id: instance.instanceId,
|
|
153
|
+
total: instance.tabs.length,
|
|
154
|
+
tabs: instance.tabs.map((tab) => ({
|
|
155
|
+
tab_id: tab.tab_id,
|
|
156
|
+
...(typeof tab.window_id === "number" ? { window_id: tab.window_id } : {}),
|
|
157
|
+
active: tab.active,
|
|
158
|
+
...(selectedAttachment &&
|
|
159
|
+
selectedAttachment.instanceId === instance.instanceId &&
|
|
160
|
+
selectedAttachment.tabId === tab.tab_id
|
|
161
|
+
? { selected: true }
|
|
162
|
+
: {}),
|
|
163
|
+
title: tab.title,
|
|
164
|
+
url: tab.url,
|
|
165
|
+
...(tab.status ? { status: tab.status } : {}),
|
|
166
|
+
})),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async startRecording(input) {
|
|
170
|
+
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
171
|
+
const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
|
|
172
|
+
const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.startRecordingRemote", {
|
|
173
|
+
...input,
|
|
174
|
+
session_id: normalizedSessionId,
|
|
175
|
+
});
|
|
176
|
+
if (remote) {
|
|
177
|
+
return remote;
|
|
178
|
+
}
|
|
179
|
+
const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
|
|
180
|
+
if (!attached) {
|
|
181
|
+
throw new Error("No attached Firefox tab is selected for this session. Select a tab in the extension popup first.");
|
|
182
|
+
}
|
|
183
|
+
if (!this.firefoxAttachRegistry) {
|
|
184
|
+
throw new Error("Firefox attach backend is not available.");
|
|
185
|
+
}
|
|
186
|
+
const record = await this.firefoxAttachRegistry.startRecording({
|
|
187
|
+
sessionId: normalizedSessionId,
|
|
188
|
+
instanceId: input.instance_id?.trim() || attached.instanceId,
|
|
189
|
+
tabId: attached.tabId,
|
|
190
|
+
tabTitle: attached.title?.trim() || "attached-tab",
|
|
191
|
+
...(attached.url ? { tabUrl: attached.url } : {}),
|
|
192
|
+
});
|
|
193
|
+
return this.mapRecordingRecord(normalizedSessionId, record);
|
|
194
|
+
}
|
|
195
|
+
async stopRecording(input) {
|
|
196
|
+
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
197
|
+
const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
|
|
198
|
+
const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.stopRecordingRemote", {
|
|
199
|
+
...input,
|
|
200
|
+
session_id: normalizedSessionId,
|
|
201
|
+
});
|
|
202
|
+
if (remote) {
|
|
203
|
+
return remote;
|
|
204
|
+
}
|
|
205
|
+
if (!this.firefoxAttachRegistry) {
|
|
206
|
+
throw new Error("Firefox attach backend is not available.");
|
|
207
|
+
}
|
|
208
|
+
const record = await this.firefoxAttachRegistry.stopRecording({
|
|
209
|
+
sessionId: normalizedSessionId,
|
|
210
|
+
});
|
|
211
|
+
if (!record) {
|
|
212
|
+
return {
|
|
213
|
+
session_id: normalizedSessionId,
|
|
214
|
+
stopped: false,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
session_id: normalizedSessionId,
|
|
219
|
+
stopped: true,
|
|
220
|
+
recording_id: record.recordingId,
|
|
221
|
+
bundle_dir_name: record.bundleDirName,
|
|
222
|
+
bundle_relative_path: record.bundleRelativePath,
|
|
223
|
+
bundle_path: record.bundlePath,
|
|
224
|
+
...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async getRecordingStatus(input) {
|
|
228
|
+
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
229
|
+
const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
|
|
230
|
+
const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.getRecordingStatusRemote", {
|
|
231
|
+
...input,
|
|
232
|
+
session_id: normalizedSessionId,
|
|
233
|
+
});
|
|
234
|
+
if (remote) {
|
|
235
|
+
return remote;
|
|
236
|
+
}
|
|
237
|
+
if (!this.firefoxAttachRegistry) {
|
|
238
|
+
throw new Error("Firefox attach backend is not available.");
|
|
239
|
+
}
|
|
240
|
+
const record = await this.firefoxAttachRegistry.getRecordingStatus(normalizedSessionId);
|
|
241
|
+
return {
|
|
242
|
+
session_id: normalizedSessionId,
|
|
243
|
+
active: record?.status === "recording",
|
|
244
|
+
...(record ? { recording: this.mapRecordingStatusRecord(record) } : {}),
|
|
245
|
+
};
|
|
75
246
|
}
|
|
76
247
|
async open(input) {
|
|
77
248
|
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
@@ -85,12 +256,26 @@ class BrowserService {
|
|
|
85
256
|
}
|
|
86
257
|
this.ensureEnabled();
|
|
87
258
|
const existingState = this.sessionStates.get(normalizedSessionId);
|
|
88
|
-
const
|
|
259
|
+
const requestedViewport = this.resolveRequestedViewport(input);
|
|
260
|
+
const shouldUpgradeLegacyViewport = this.config.browser.headless === false &&
|
|
261
|
+
input.reset_context !== true &&
|
|
262
|
+
!requestedViewport &&
|
|
263
|
+
Boolean(existingState?.page.viewportSize());
|
|
264
|
+
const shouldReset = input.reset_context === true || shouldUpgradeLegacyViewport;
|
|
89
265
|
const targetUrl = this.resolveBrowserUrl(input.url);
|
|
90
266
|
if (shouldReset && existingState) {
|
|
91
267
|
await this.closeState(normalizedSessionId, existingState);
|
|
92
268
|
}
|
|
93
|
-
const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset);
|
|
269
|
+
const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset, input);
|
|
270
|
+
if (requestedViewport) {
|
|
271
|
+
await state.page.setViewportSize(requestedViewport);
|
|
272
|
+
}
|
|
273
|
+
else if (this.config.browser.headless !== false) {
|
|
274
|
+
await state.page.setViewportSize({
|
|
275
|
+
width: DEFAULT_BROWSER_VIEWPORT_WIDTH,
|
|
276
|
+
height: DEFAULT_BROWSER_VIEWPORT_HEIGHT,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
94
279
|
const waitUntil = (input.wait_until ??
|
|
95
280
|
this.config.browser.waitUntil);
|
|
96
281
|
await state.page.goto(targetUrl, {
|
|
@@ -108,13 +293,18 @@ class BrowserService {
|
|
|
108
293
|
createdContext,
|
|
109
294
|
waitUntil,
|
|
110
295
|
headless: this.config.browser.headless,
|
|
296
|
+
viewportWidth: state.page.viewportSize()?.width ?? requestedViewport?.width,
|
|
297
|
+
viewportHeight: state.page.viewportSize()?.height ?? requestedViewport?.height,
|
|
111
298
|
});
|
|
299
|
+
const viewport = state.page.viewportSize();
|
|
112
300
|
return {
|
|
113
301
|
session_id: normalizedSessionId,
|
|
114
302
|
opened: true,
|
|
115
303
|
created_context: createdContext,
|
|
116
304
|
url: state.currentUrl,
|
|
117
305
|
...(state.title ? { title: state.title } : {}),
|
|
306
|
+
...(viewport?.width ? { viewport_width: viewport.width } : {}),
|
|
307
|
+
...(viewport?.height ? { viewport_height: viewport.height } : {}),
|
|
118
308
|
};
|
|
119
309
|
}
|
|
120
310
|
async getConsole(input) {
|
|
@@ -152,6 +342,22 @@ class BrowserService {
|
|
|
152
342
|
return remote;
|
|
153
343
|
}
|
|
154
344
|
this.ensureEnabled();
|
|
345
|
+
const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
|
|
346
|
+
if (attached) {
|
|
347
|
+
const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
|
|
348
|
+
action: "click",
|
|
349
|
+
payload: this.buildAttachedLocatorPayload(input),
|
|
350
|
+
});
|
|
351
|
+
return {
|
|
352
|
+
session_id: normalizedSessionId,
|
|
353
|
+
clicked: true,
|
|
354
|
+
...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
|
|
355
|
+
...(input.selector ? { selector: input.selector } : {}),
|
|
356
|
+
...(input.text ? { text: input.text } : {}),
|
|
357
|
+
url: String(result?.url || attached.url || ""),
|
|
358
|
+
...(result?.title ? { title: String(result.title) } : {}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
155
361
|
const { sessionId, state } = await this.requireSessionState(input);
|
|
156
362
|
const locator = this.resolveLocator(state.page, input);
|
|
157
363
|
await locator.click({
|
|
@@ -181,6 +387,26 @@ class BrowserService {
|
|
|
181
387
|
return remote;
|
|
182
388
|
}
|
|
183
389
|
this.ensureEnabled();
|
|
390
|
+
const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
|
|
391
|
+
if (attached) {
|
|
392
|
+
const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
|
|
393
|
+
action: "fill",
|
|
394
|
+
payload: {
|
|
395
|
+
...this.buildAttachedLocatorPayload(input),
|
|
396
|
+
value: input.value,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
return {
|
|
400
|
+
session_id: normalizedSessionId,
|
|
401
|
+
filled: true,
|
|
402
|
+
...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
|
|
403
|
+
...(input.selector ? { selector: input.selector } : {}),
|
|
404
|
+
...(input.text ? { text: input.text } : {}),
|
|
405
|
+
value_length: input.value.length,
|
|
406
|
+
url: String(result?.url || attached.url || ""),
|
|
407
|
+
...(result?.title ? { title: String(result.title) } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
184
410
|
const { sessionId, state } = await this.requireSessionState(input);
|
|
185
411
|
const locator = this.resolveLocator(state.page, input);
|
|
186
412
|
await locator.fill(input.value, {
|
|
@@ -211,6 +437,26 @@ class BrowserService {
|
|
|
211
437
|
return remote;
|
|
212
438
|
}
|
|
213
439
|
this.ensureEnabled();
|
|
440
|
+
const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
|
|
441
|
+
if (attached) {
|
|
442
|
+
const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
|
|
443
|
+
action: "press",
|
|
444
|
+
payload: {
|
|
445
|
+
...this.buildAttachedLocatorPayload(input),
|
|
446
|
+
key: input.key,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
session_id: normalizedSessionId,
|
|
451
|
+
pressed: true,
|
|
452
|
+
key: input.key,
|
|
453
|
+
...(input.ai_tag ? { ai_tag: input.ai_tag } : {}),
|
|
454
|
+
...(input.selector ? { selector: input.selector } : {}),
|
|
455
|
+
...(input.text ? { text: input.text } : {}),
|
|
456
|
+
url: String(result?.url || attached.url || ""),
|
|
457
|
+
...(result?.title ? { title: String(result.title) } : {}),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
214
460
|
const { sessionId, state } = await this.requireSessionState(input);
|
|
215
461
|
if (input.selector || input.text) {
|
|
216
462
|
const locator = this.resolveLocator(state.page, input);
|
|
@@ -235,6 +481,64 @@ class BrowserService {
|
|
|
235
481
|
...(state.title ? { title: state.title } : {}),
|
|
236
482
|
};
|
|
237
483
|
}
|
|
484
|
+
async injectScript(input) {
|
|
485
|
+
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
486
|
+
const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
|
|
487
|
+
const remote = await this.invokeRemote(normalizedSessionId, "telegramMcp.browser.injectScriptRemote", {
|
|
488
|
+
...input,
|
|
489
|
+
session_id: normalizedSessionId,
|
|
490
|
+
});
|
|
491
|
+
if (remote) {
|
|
492
|
+
return remote;
|
|
493
|
+
}
|
|
494
|
+
this.ensureEnabled();
|
|
495
|
+
const namespace = input.namespace?.trim() || "TELLY";
|
|
496
|
+
const source = input.source?.trim().length
|
|
497
|
+
? input.source
|
|
498
|
+
: input.file_path?.trim()
|
|
499
|
+
? await (0, promises_1.readFile)(node_path_1.default.resolve(input.file_path.trim()), "utf8")
|
|
500
|
+
: null;
|
|
501
|
+
if (!source) {
|
|
502
|
+
throw new Error("Provide source or file_path.");
|
|
503
|
+
}
|
|
504
|
+
const sourceType = input.source?.trim().length ? "inline" : "file";
|
|
505
|
+
const wrappedSource = this.wrapInjectedScript(source, namespace);
|
|
506
|
+
const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
|
|
507
|
+
if (attached) {
|
|
508
|
+
const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
|
|
509
|
+
action: "inject_script",
|
|
510
|
+
payload: {
|
|
511
|
+
namespace,
|
|
512
|
+
source,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
return {
|
|
516
|
+
session_id: normalizedSessionId,
|
|
517
|
+
injected: true,
|
|
518
|
+
namespace,
|
|
519
|
+
source_type: sourceType,
|
|
520
|
+
bytes: Buffer.byteLength(source, "utf8"),
|
|
521
|
+
url: String(result?.url || attached.url || ""),
|
|
522
|
+
...(result?.title ? { title: String(result.title) } : {}),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const { sessionId, state } = await this.requireSessionState(input);
|
|
526
|
+
await state.page.addScriptTag({
|
|
527
|
+
content: wrappedSource,
|
|
528
|
+
});
|
|
529
|
+
state.currentUrl = state.page.url();
|
|
530
|
+
state.title = await state.page.title().catch(() => state.title);
|
|
531
|
+
state.lastUsedAt = new Date().toISOString();
|
|
532
|
+
return {
|
|
533
|
+
session_id: sessionId,
|
|
534
|
+
injected: true,
|
|
535
|
+
namespace,
|
|
536
|
+
source_type: sourceType,
|
|
537
|
+
bytes: Buffer.byteLength(source, "utf8"),
|
|
538
|
+
url: state.currentUrl,
|
|
539
|
+
...(state.title ? { title: state.title } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
238
542
|
async reload(input) {
|
|
239
543
|
const resolved = this.projectIdentityResolver.resolveSessionDefaults(input);
|
|
240
544
|
const normalizedSessionId = await this.normalizeSessionIdForAccess(resolved.sessionId);
|
|
@@ -432,6 +736,37 @@ class BrowserService {
|
|
|
432
736
|
return remote;
|
|
433
737
|
}
|
|
434
738
|
this.ensureEnabled();
|
|
739
|
+
const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
|
|
740
|
+
if (attached) {
|
|
741
|
+
const selector = input.selector?.trim() || "body";
|
|
742
|
+
const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
|
|
743
|
+
action: "dom",
|
|
744
|
+
payload: {
|
|
745
|
+
selector,
|
|
746
|
+
include_html: input.include_html !== false,
|
|
747
|
+
include_text: input.include_text !== false,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
return {
|
|
751
|
+
session_id: normalizedSessionId,
|
|
752
|
+
selector,
|
|
753
|
+
found: result?.found === true,
|
|
754
|
+
...(result?.url ? { url: String(result.url) } : {}),
|
|
755
|
+
...(result?.title ? { title: String(result.title) } : {}),
|
|
756
|
+
...(typeof result?.outer_html === "string"
|
|
757
|
+
? { outer_html: result.outer_html }
|
|
758
|
+
: {}),
|
|
759
|
+
...(typeof result?.text_content === "string"
|
|
760
|
+
? { text_content: result.text_content }
|
|
761
|
+
: {}),
|
|
762
|
+
...(typeof result?.visible === "boolean"
|
|
763
|
+
? { visible: result.visible }
|
|
764
|
+
: {}),
|
|
765
|
+
...(result?.attributes && typeof result.attributes === "object"
|
|
766
|
+
? { attributes: result.attributes }
|
|
767
|
+
: {}),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
435
770
|
const { sessionId, state } = await this.requireSessionState(input);
|
|
436
771
|
const selector = input.selector?.trim() || "body";
|
|
437
772
|
const snapshot = await state.page
|
|
@@ -562,6 +897,75 @@ class BrowserService {
|
|
|
562
897
|
return remote;
|
|
563
898
|
}
|
|
564
899
|
this.ensureEnabled();
|
|
900
|
+
const attached = await this.getAttachedBrowserAttachment(normalizedSessionId);
|
|
901
|
+
if (attached) {
|
|
902
|
+
const session = await this.sessionStore.getSession(normalizedSessionId);
|
|
903
|
+
const fileName = sanitizeScreenshotName(input.file_name);
|
|
904
|
+
const result = await this.runAttachedTabAction(normalizedSessionId, attached, {
|
|
905
|
+
action: "screenshot",
|
|
906
|
+
});
|
|
907
|
+
const pngBase64 = typeof result?.png_base64 === "string" ? result.png_base64 : "";
|
|
908
|
+
if (!pngBase64) {
|
|
909
|
+
throw new Error("Attached browser screenshot did not return PNG data.");
|
|
910
|
+
}
|
|
911
|
+
const pngBuffer = Buffer.from(pngBase64, "base64");
|
|
912
|
+
const workspaceDir = this.objectStore.resolveWorkspaceDir(session);
|
|
913
|
+
const exchangeDir = node_path_1.default.resolve(workspaceDir, this.config.exchange.dir);
|
|
914
|
+
const storedFile = await this.objectStore.storeFile({
|
|
915
|
+
session,
|
|
916
|
+
sessionId: normalizedSessionId,
|
|
917
|
+
source: "browser-screenshot",
|
|
918
|
+
relativePath: buildDatedRelativePath(fileName),
|
|
919
|
+
content: pngBuffer,
|
|
920
|
+
mimeType: "image/png",
|
|
921
|
+
});
|
|
922
|
+
await this.xchangeFileMetaStore.setXchangeFileMeta({
|
|
923
|
+
sessionId: normalizedSessionId,
|
|
924
|
+
filePath: storedFile.filePath,
|
|
925
|
+
relativePath: storedFile.relativePath,
|
|
926
|
+
source: "browser-screenshot",
|
|
927
|
+
uploadedAt: new Date().toISOString(),
|
|
928
|
+
storageRef: storedFile.storageRef,
|
|
929
|
+
bucketName: storedFile.bucketName,
|
|
930
|
+
objectName: storedFile.objectName,
|
|
931
|
+
vfsNodeId: storedFile.vfsNodeId,
|
|
932
|
+
vfsPublicUrl: storedFile.vfsPublicUrl,
|
|
933
|
+
vfsParentId: storedFile.vfsParentId,
|
|
934
|
+
mimeType: "image/png",
|
|
935
|
+
sizeBytes: storedFile.sizeBytes,
|
|
936
|
+
...(input.caption ? { caption: input.caption } : {}),
|
|
937
|
+
});
|
|
938
|
+
let telegramMessageId;
|
|
939
|
+
if (input.send_to_telegram === true) {
|
|
940
|
+
if (this.config.distributed.mode === "client") {
|
|
941
|
+
telegramMessageId = await this.sendScreenshotToGatewayTelegramRoute({
|
|
942
|
+
sessionId: normalizedSessionId,
|
|
943
|
+
fileName,
|
|
944
|
+
pngBuffer,
|
|
945
|
+
...(input.caption ? { caption: input.caption } : {}),
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
const binding = await this.bindingStore.getBinding(normalizedSessionId);
|
|
950
|
+
if (!binding) {
|
|
951
|
+
throw new Error("Session is not linked to Telegram, so screenshot cannot be sent there.");
|
|
952
|
+
}
|
|
953
|
+
const sent = await this.telegramTransport.sendDocumentToChat(binding.telegramChatId, storedFile.filePath, input.caption);
|
|
954
|
+
telegramMessageId = sent.messageId;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
session_id: normalizedSessionId,
|
|
959
|
+
file_path: storedFile.filePath,
|
|
960
|
+
workspace_dir: workspaceDir,
|
|
961
|
+
exchange_dir: exchangeDir,
|
|
962
|
+
...(typeof telegramMessageId === "number"
|
|
963
|
+
? { telegram_message_id: telegramMessageId }
|
|
964
|
+
: {}),
|
|
965
|
+
...(result?.url ? { url: String(result.url) } : {}),
|
|
966
|
+
...(result?.title ? { title: String(result.title) } : {}),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
565
969
|
const { sessionId, state, session } = await this.requireSessionState(input);
|
|
566
970
|
const fileName = sanitizeScreenshotName(input.file_name);
|
|
567
971
|
const pngBuffer = input.selector?.trim()
|
|
@@ -690,10 +1094,13 @@ class BrowserService {
|
|
|
690
1094
|
async ensureBrowser() {
|
|
691
1095
|
this.browserPromise ??= (async () => {
|
|
692
1096
|
const playwright = await this.ensurePlaywright();
|
|
693
|
-
const launchArgs =
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1097
|
+
const launchArgs = [];
|
|
1098
|
+
if (this.config.browser.headless === false) {
|
|
1099
|
+
launchArgs.push("--start-maximized");
|
|
1100
|
+
if (this.config.browser.devtools === true) {
|
|
1101
|
+
launchArgs.push("--auto-open-devtools-for-tabs");
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
697
1104
|
const browser = await playwright.chromium.launch({
|
|
698
1105
|
headless: this.config.browser.headless,
|
|
699
1106
|
slowMo: this.config.browser.slowMoMs,
|
|
@@ -717,13 +1124,13 @@ class BrowserService {
|
|
|
717
1124
|
})();
|
|
718
1125
|
return this.browserPromise;
|
|
719
1126
|
}
|
|
720
|
-
async ensureSessionState(sessionId, forceNewContext) {
|
|
1127
|
+
async ensureSessionState(sessionId, forceNewContext, openInput) {
|
|
721
1128
|
const existing = this.sessionStates.get(sessionId);
|
|
722
1129
|
if (existing && !forceNewContext) {
|
|
723
1130
|
return { state: existing, createdContext: false };
|
|
724
1131
|
}
|
|
725
1132
|
const browser = await this.ensureBrowser();
|
|
726
|
-
const context = await browser.newContext();
|
|
1133
|
+
const context = await browser.newContext(this.buildContextOptions(openInput));
|
|
727
1134
|
const page = await context.newPage();
|
|
728
1135
|
const createdAt = new Date().toISOString();
|
|
729
1136
|
const state = {
|
|
@@ -763,6 +1170,32 @@ class BrowserService {
|
|
|
763
1170
|
this.sessionStates.set(sessionId, state);
|
|
764
1171
|
return { state, createdContext: true };
|
|
765
1172
|
}
|
|
1173
|
+
resolveRequestedViewport(input) {
|
|
1174
|
+
const width = input?.width;
|
|
1175
|
+
const height = input?.height;
|
|
1176
|
+
if (!width && !height) {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
if (!width || !height) {
|
|
1180
|
+
throw new Error("Browser viewport requires both width and height together.");
|
|
1181
|
+
}
|
|
1182
|
+
return { width, height };
|
|
1183
|
+
}
|
|
1184
|
+
buildContextOptions(input) {
|
|
1185
|
+
const requestedViewport = this.resolveRequestedViewport(input);
|
|
1186
|
+
if (requestedViewport) {
|
|
1187
|
+
return {
|
|
1188
|
+
viewport: requestedViewport,
|
|
1189
|
+
screen: requestedViewport,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
if (this.config.browser.headless === false) {
|
|
1193
|
+
return {
|
|
1194
|
+
viewport: null,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return {};
|
|
1198
|
+
}
|
|
766
1199
|
recordNetworkFailure(state, request, response) {
|
|
767
1200
|
const failure = request.failure();
|
|
768
1201
|
pushBounded(state.networkFailures, {
|
|
@@ -825,6 +1258,107 @@ class BrowserService {
|
|
|
825
1258
|
const localSession = await this.sessionStore.getSession(localSessionId);
|
|
826
1259
|
return localSession ? localSessionId : trimmed;
|
|
827
1260
|
}
|
|
1261
|
+
async resolveOptionalSessionIdForRemote(input) {
|
|
1262
|
+
const trimmed = input.session_id?.trim();
|
|
1263
|
+
if (!trimmed) {
|
|
1264
|
+
return undefined;
|
|
1265
|
+
}
|
|
1266
|
+
return await this.normalizeSessionIdForAccess(trimmed);
|
|
1267
|
+
}
|
|
1268
|
+
listFirefoxAttachInstances() {
|
|
1269
|
+
if (!this.firefoxAttachRegistry) {
|
|
1270
|
+
return [];
|
|
1271
|
+
}
|
|
1272
|
+
return this.firefoxAttachRegistry.listInstances();
|
|
1273
|
+
}
|
|
1274
|
+
buildAttachedLocatorPayload(input) {
|
|
1275
|
+
return {
|
|
1276
|
+
...(input.ai_tag?.trim() ? { ai_tag: input.ai_tag.trim() } : {}),
|
|
1277
|
+
...(input.selector?.trim() ? { selector: input.selector.trim() } : {}),
|
|
1278
|
+
...(input.text?.trim() ? { text: input.text.trim() } : {}),
|
|
1279
|
+
...(typeof input.exact === "boolean" ? { exact: input.exact } : {}),
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
async getAttachedBrowserAttachment(sessionId) {
|
|
1283
|
+
return await this.maintenanceStore.getBrowserAttachment(sessionId);
|
|
1284
|
+
}
|
|
1285
|
+
mapRecordingRecord(sessionId, record) {
|
|
1286
|
+
return {
|
|
1287
|
+
session_id: sessionId,
|
|
1288
|
+
started: true,
|
|
1289
|
+
backend: "firefox-attached",
|
|
1290
|
+
recording_id: record.recordingId,
|
|
1291
|
+
instance_id: record.instanceId,
|
|
1292
|
+
tab_id: record.tabId,
|
|
1293
|
+
...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
|
|
1294
|
+
...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
|
|
1295
|
+
bundle_dir_name: record.bundleDirName,
|
|
1296
|
+
bundle_relative_path: record.bundleRelativePath,
|
|
1297
|
+
bundle_path: record.bundlePath,
|
|
1298
|
+
started_at: record.startedAt,
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
mapRecordingStatusRecord(record) {
|
|
1302
|
+
return {
|
|
1303
|
+
backend: "firefox-attached",
|
|
1304
|
+
recording_id: record.recordingId,
|
|
1305
|
+
instance_id: record.instanceId,
|
|
1306
|
+
tab_id: record.tabId,
|
|
1307
|
+
...(record.tabTitle ? { tab_title: record.tabTitle } : {}),
|
|
1308
|
+
...(record.tabUrl ? { tab_url: record.tabUrl } : {}),
|
|
1309
|
+
bundle_dir_name: record.bundleDirName,
|
|
1310
|
+
bundle_relative_path: record.bundleRelativePath,
|
|
1311
|
+
bundle_path: record.bundlePath,
|
|
1312
|
+
started_at: record.startedAt,
|
|
1313
|
+
...(record.stoppedAt ? { stopped_at: record.stoppedAt } : {}),
|
|
1314
|
+
status: record.status,
|
|
1315
|
+
event_count: record.eventCount,
|
|
1316
|
+
...(record.lastEventAt ? { last_event_at: record.lastEventAt } : {}),
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
async runAttachedTabAction(sessionId, attachment, input) {
|
|
1320
|
+
if (!this.firefoxAttachRegistry) {
|
|
1321
|
+
throw new Error("Attached browser backend is not available.");
|
|
1322
|
+
}
|
|
1323
|
+
const result = await this.firefoxAttachRegistry.invokeTabAction({
|
|
1324
|
+
instanceId: attachment.instanceId,
|
|
1325
|
+
tabId: attachment.tabId,
|
|
1326
|
+
action: input.action,
|
|
1327
|
+
payload: input.payload,
|
|
1328
|
+
});
|
|
1329
|
+
await this.maintenanceStore.setBrowserAttachment({
|
|
1330
|
+
...attachment,
|
|
1331
|
+
...(result?.title ? { title: String(result.title) } : {}),
|
|
1332
|
+
...(result?.url ? { url: String(result.url) } : {}),
|
|
1333
|
+
});
|
|
1334
|
+
this.logger.info("Attached browser tab action executed", {
|
|
1335
|
+
sessionId,
|
|
1336
|
+
instanceId: attachment.instanceId,
|
|
1337
|
+
tabId: attachment.tabId,
|
|
1338
|
+
action: input.action,
|
|
1339
|
+
});
|
|
1340
|
+
return result;
|
|
1341
|
+
}
|
|
1342
|
+
wrapInjectedScript(source, namespace) {
|
|
1343
|
+
return `const __tellyNamespace = ${JSON.stringify(namespace)};
|
|
1344
|
+
window[__tellyNamespace] = window[__tellyNamespace] || {};
|
|
1345
|
+
var TELLY = window[__tellyNamespace];
|
|
1346
|
+
const __tellyBeforeKeys = new Set(Object.getOwnPropertyNames(window));
|
|
1347
|
+
${source}
|
|
1348
|
+
for (const __tellyKey of Object.getOwnPropertyNames(window)) {
|
|
1349
|
+
if (__tellyBeforeKeys.has(__tellyKey)) {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (__tellyKey === __tellyNamespace) {
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
try {
|
|
1356
|
+
window[__tellyNamespace][__tellyKey] = window[__tellyKey];
|
|
1357
|
+
} catch {
|
|
1358
|
+
// ignore unassignable globals
|
|
1359
|
+
}
|
|
1360
|
+
}`;
|
|
1361
|
+
}
|
|
828
1362
|
async sendScreenshotToGatewayTelegramRoute(input) {
|
|
829
1363
|
if (!this.config.distributed.gatewayPublicUrl) {
|
|
830
1364
|
throw new Error("send_to_telegram on client nodes requires GATEWAY_PUBLIC_URL.");
|