@deadragdoll/tellymcp 0.0.11 → 0.0.12
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 +29 -0
- package/README.md +28 -0
- package/TOOLS.md +26 -0
- package/VERSION.md +2 -2
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +10 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +4 -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/browserService.js +58 -8
- 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 +28 -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/package.json +1 -1
package/README-ru.md
CHANGED
|
@@ -272,6 +272,35 @@ tellymcp browser install
|
|
|
272
272
|
|
|
273
273
|
Не подменяй browser workflow ad hoc shell-командами с Playwright, кроме случаев, когда ты отлаживаешь сам browser runtime.
|
|
274
274
|
|
|
275
|
+
## Terminal Blockers
|
|
276
|
+
|
|
277
|
+
Gateway prompt scanner теперь живёт от live-client lifecycle:
|
|
278
|
+
|
|
279
|
+
- на старте gateway scanner только armed, но не крутится вхолостую
|
|
280
|
+
- реальная работа начинается после подключения live client
|
|
281
|
+
- relay console materialization идёт из `hello` и owner-route hydration, а не из `/menu`
|
|
282
|
+
- детект работает по хвосту захваченного terminal buffer
|
|
283
|
+
|
|
284
|
+
Основная эвристика blocker-а:
|
|
285
|
+
|
|
286
|
+
- подряд идущие numbered choices: `1.`, `2.`, `3.`
|
|
287
|
+
- рядом есть action hints вроде `press`, `input`, `choose`, `enter`, `esc`, `yes`, `no`
|
|
288
|
+
- в Telegram notice попадают и 1-2 строки контекста выше menu block
|
|
289
|
+
|
|
290
|
+
Когда blocker найден, gateway может отправить inline-кнопки:
|
|
291
|
+
|
|
292
|
+
- `1..N`
|
|
293
|
+
- `Enter`
|
|
294
|
+
- `Esc`
|
|
295
|
+
|
|
296
|
+
Эти кнопки отправляют в консоль ровно цифру или terminal action. Навигация маркером не используется.
|
|
297
|
+
|
|
298
|
+
Операционные заметки:
|
|
299
|
+
|
|
300
|
+
- одинаковый blocker fingerprint не перевысылается повторно
|
|
301
|
+
- relay capture miss для offline agent считается debug-only шумом
|
|
302
|
+
- `Storage` и `Screenshots` на gateway теперь relay-aware и читают metadata через gateway routes, а не через filesystem самого gateway
|
|
303
|
+
|
|
275
304
|
## Collaboration
|
|
276
305
|
|
|
277
306
|
Проекты:
|
package/README.md
CHANGED
|
@@ -272,6 +272,34 @@ tellymcp browser install
|
|
|
272
272
|
|
|
273
273
|
Do not replace browser workflows with ad hoc shell Playwright commands unless you are debugging the runtime itself.
|
|
274
274
|
|
|
275
|
+
## Terminal Blockers
|
|
276
|
+
|
|
277
|
+
Gateway prompt scanning is now live-client driven:
|
|
278
|
+
|
|
279
|
+
- the scanner is armed on gateway startup but starts working only after a live client connects
|
|
280
|
+
- relay console materialization happens from gateway hello/owner-route hydration, not from `/menu`
|
|
281
|
+
- prompt detection works on the tail of the captured terminal buffer
|
|
282
|
+
|
|
283
|
+
Primary blocker heuristic:
|
|
284
|
+
|
|
285
|
+
- contiguous numbered choices like `1.`, `2.`, `3.`
|
|
286
|
+
- nearby action hints such as `press`, `input`, `choose`, `enter`, `esc`, `yes`, `no`
|
|
287
|
+
- optional context lines above the menu block are included in the Telegram notice
|
|
288
|
+
|
|
289
|
+
When a blocker is detected, the gateway can send inline Telegram buttons for:
|
|
290
|
+
|
|
291
|
+
- `1..N`
|
|
292
|
+
- `Enter`
|
|
293
|
+
- `Esc`
|
|
294
|
+
|
|
295
|
+
Those buttons send exactly the digit or terminal action to the target console. No marker navigation is used.
|
|
296
|
+
|
|
297
|
+
Operational notes:
|
|
298
|
+
|
|
299
|
+
- repeated scans of the same blocker fingerprint do not resend the notice
|
|
300
|
+
- relay capture misses for offline agents are treated as debug-only noise
|
|
301
|
+
- `Storage` and `Screenshots` on the gateway are relay-aware and read console metadata through gateway routes instead of the gateway filesystem
|
|
302
|
+
|
|
275
303
|
## Collaboration Model
|
|
276
304
|
|
|
277
305
|
Projects:
|
package/TOOLS.md
CHANGED
|
@@ -38,6 +38,21 @@ Browser runtime rule:
|
|
|
38
38
|
- If browser tools fail because the Playwright browser runtime is missing, install it with `tellymcp browser install`.
|
|
39
39
|
- Do not stop at the installation error itself. Install the browser runtime first, then retry the browser tool.
|
|
40
40
|
|
|
41
|
+
Terminal prompt scan rules:
|
|
42
|
+
|
|
43
|
+
- gateway prompt scan is driven by live client lifecycle, not by manual menu entry
|
|
44
|
+
- the scanner works on the tail of the captured terminal buffer
|
|
45
|
+
- the main blocker signal is:
|
|
46
|
+
- a contiguous numbered choice block like `1.`, `2.`, `3.`
|
|
47
|
+
- nearby action-hint language such as `press`, `input`, `choose`, `enter`, `esc`, `yes`, `no`
|
|
48
|
+
- exact footer text is helpful but not required
|
|
49
|
+
- when a blocker notice contains inline buttons, those buttons intentionally send only:
|
|
50
|
+
- a digit `1..N`
|
|
51
|
+
- `Enter`
|
|
52
|
+
- `Esc`
|
|
53
|
+
- do not assume marker navigation
|
|
54
|
+
- do not reinterpret those buttons as alternate hotkeys like `(y)` or `(p)`
|
|
55
|
+
|
|
41
56
|
Collaboration tools:
|
|
42
57
|
|
|
43
58
|
- `list_gateway_sessions`
|
|
@@ -725,6 +740,8 @@ Input:
|
|
|
725
740
|
|
|
726
741
|
- `session_id?`
|
|
727
742
|
- `url`
|
|
743
|
+
- `width?`
|
|
744
|
+
- `height?`
|
|
728
745
|
- `wait_until?`
|
|
729
746
|
- `reset_context?`
|
|
730
747
|
|
|
@@ -735,12 +752,19 @@ Output:
|
|
|
735
752
|
- `created_context`
|
|
736
753
|
- `url`
|
|
737
754
|
- `title?`
|
|
755
|
+
- `viewport_width?`
|
|
756
|
+
- `viewport_height?`
|
|
738
757
|
|
|
739
758
|
Notes:
|
|
740
759
|
|
|
741
760
|
- each session gets its own isolated browser context and page
|
|
742
761
|
- call this first before reading console, DOM, styles, or screenshots
|
|
743
762
|
- `url` may be an absolute URL, or a relative path when `BROWSER_ADDRESS` is configured
|
|
763
|
+
- in headed mode the browser window is started maximized by default
|
|
764
|
+
- in headed mode a fresh browser context is created without a fixed viewport unless you explicitly pass `width` and `height`
|
|
765
|
+
- if you want page content to grow and shrink together with the outer browser window, do not pass `width` and `height`
|
|
766
|
+
- if you pass viewport size, pass both `width` and `height` together
|
|
767
|
+
- use explicit `width` and `height` when the task depends on a specific responsive breakpoint or working area
|
|
744
768
|
|
|
745
769
|
## `browser_console`
|
|
746
770
|
|
|
@@ -1083,6 +1107,8 @@ Telegram UI summary:
|
|
|
1083
1107
|
- default logical console identity comes from `.mcpsession.json` in the workspace or explicit `-s`
|
|
1084
1108
|
- terminal runtime metadata does not change `session_id`
|
|
1085
1109
|
- `Browser -> Screenshots` lists screenshots created by `browser_screenshot`
|
|
1110
|
+
- `Storage` and `Screenshots` are relay-aware on the gateway
|
|
1111
|
+
- for relay consoles they read file metadata through gateway routes, not through the gateway filesystem
|
|
1086
1112
|
- `Storage` browses `.mcp-xchange` for the active console and can send stored notes/files back into Telegram
|
|
1087
1113
|
- `Settings` contains `Info`, `Rename`, `Unpair`, `Back`
|
|
1088
1114
|
- project/collab work is the only supported user-facing collaboration path in Telegram UI
|
package/VERSION.md
CHANGED
|
@@ -100,8 +100,8 @@ For detailed engineering history, refactors, and internal development notes, see
|
|
|
100
100
|
- `tellymcp doctor`
|
|
101
101
|
- `tellymcp mcp --help`
|
|
102
102
|
- Standalone and public installation guides:
|
|
103
|
-
- [STANDALONE.md](STANDALONE.md)
|
|
104
|
-
- [STANDALONE-ru.md](STANDALONE-ru.md)
|
|
103
|
+
- [STANDALONE.md](docs/STANDALONE.md)
|
|
104
|
+
- [STANDALONE-ru.md](docs/STANDALONE-ru.md)
|
|
105
105
|
- Browser runtime helper:
|
|
106
106
|
- `tellymcp browser install`
|
|
107
107
|
- Public README set for GitHub and npm:
|
|
@@ -1226,6 +1226,13 @@ const TelegramMcpGatewaySocketService = {
|
|
|
1226
1226
|
}))
|
|
1227
1227
|
: [],
|
|
1228
1228
|
}, { meta: { internal_call: true } });
|
|
1229
|
+
await runtime.telegramTransport.hydrateGatewayClientOwnerRoute({
|
|
1230
|
+
clientUuid: hello.client_uuid,
|
|
1231
|
+
...(hello.gateway_user_uuid
|
|
1232
|
+
? { gatewayUserUuid: hello.gateway_user_uuid }
|
|
1233
|
+
: {}),
|
|
1234
|
+
});
|
|
1235
|
+
runtime.telegramTransport.ensurePromptScanRunning();
|
|
1229
1236
|
}
|
|
1230
1237
|
const localVersionInfo = this.getLocalVersionInfo?.() ?? {
|
|
1231
1238
|
packageVersion: "0.0.0-unknown",
|
|
@@ -2002,6 +2009,9 @@ const TelegramMcpGatewaySocketService = {
|
|
|
2002
2009
|
}
|
|
2003
2010
|
}
|
|
2004
2011
|
}
|
|
2012
|
+
if ((this.connectedClientsByUuid?.size ?? 0) === 0) {
|
|
2013
|
+
runtime.telegramTransport.pausePromptScan();
|
|
2014
|
+
}
|
|
2005
2015
|
this.connectedClientToolsAlerts?.delete(socket);
|
|
2006
2016
|
this.connectedClients?.delete(socket);
|
|
2007
2017
|
runtime.logger.warn("Gateway WS client disconnected", {
|
|
@@ -194,6 +194,8 @@ exports.clearSessionContextOutputSchema = z.object({
|
|
|
194
194
|
exports.browserOpenInputSchema = z.object({
|
|
195
195
|
session_id: z.string().trim().min(1).optional(),
|
|
196
196
|
url: z.string().trim().min(1),
|
|
197
|
+
width: z.number().int().positive().max(10000).optional(),
|
|
198
|
+
height: z.number().int().positive().max(10000).optional(),
|
|
197
199
|
wait_until: z
|
|
198
200
|
.enum(["load", "domcontentloaded", "networkidle", "commit"])
|
|
199
201
|
.optional(),
|
|
@@ -205,6 +207,8 @@ exports.browserOpenOutputSchema = z.object({
|
|
|
205
207
|
created_context: z.boolean(),
|
|
206
208
|
url: z.string().url(),
|
|
207
209
|
title: z.string().optional(),
|
|
210
|
+
viewport_width: z.number().int().positive().optional(),
|
|
211
|
+
viewport_height: z.number().int().positive().optional(),
|
|
208
212
|
});
|
|
209
213
|
exports.browserReloadInputSchema = z.object({
|
|
210
214
|
session_id: z.string().trim().min(1).optional(),
|
|
@@ -18,7 +18,7 @@ class BrowserOpenTool {
|
|
|
18
18
|
register(server) {
|
|
19
19
|
server.registerTool("browser_open", {
|
|
20
20
|
title: "Browser Open",
|
|
21
|
-
description: "Open a URL in the isolated Playwright browser context for the current session. Reuse the existing tab unless reset_context=true.",
|
|
21
|
+
description: "Open a URL in the isolated Playwright browser context for the current session. Reuse the existing tab unless reset_context=true. Optionally pass width and height to control the viewport.",
|
|
22
22
|
inputSchema: schema_1.browserOpenInputSchema,
|
|
23
23
|
outputSchema: schema_1.browserOpenOutputSchema,
|
|
24
24
|
}, async (args) => {
|
|
@@ -47,6 +47,8 @@ function formatConsoleLocation(message) {
|
|
|
47
47
|
function escapeCssAttributeValue(value) {
|
|
48
48
|
return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
|
|
49
49
|
}
|
|
50
|
+
const DEFAULT_BROWSER_VIEWPORT_WIDTH = 1720;
|
|
51
|
+
const DEFAULT_BROWSER_VIEWPORT_HEIGHT = 980;
|
|
50
52
|
class BrowserService {
|
|
51
53
|
config;
|
|
52
54
|
sessionStore;
|
|
@@ -85,12 +87,26 @@ class BrowserService {
|
|
|
85
87
|
}
|
|
86
88
|
this.ensureEnabled();
|
|
87
89
|
const existingState = this.sessionStates.get(normalizedSessionId);
|
|
88
|
-
const
|
|
90
|
+
const requestedViewport = this.resolveRequestedViewport(input);
|
|
91
|
+
const shouldUpgradeLegacyViewport = this.config.browser.headless === false &&
|
|
92
|
+
input.reset_context !== true &&
|
|
93
|
+
!requestedViewport &&
|
|
94
|
+
Boolean(existingState?.page.viewportSize());
|
|
95
|
+
const shouldReset = input.reset_context === true || shouldUpgradeLegacyViewport;
|
|
89
96
|
const targetUrl = this.resolveBrowserUrl(input.url);
|
|
90
97
|
if (shouldReset && existingState) {
|
|
91
98
|
await this.closeState(normalizedSessionId, existingState);
|
|
92
99
|
}
|
|
93
|
-
const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset);
|
|
100
|
+
const { state, createdContext } = await this.ensureSessionState(normalizedSessionId, shouldReset, input);
|
|
101
|
+
if (requestedViewport) {
|
|
102
|
+
await state.page.setViewportSize(requestedViewport);
|
|
103
|
+
}
|
|
104
|
+
else if (this.config.browser.headless !== false) {
|
|
105
|
+
await state.page.setViewportSize({
|
|
106
|
+
width: DEFAULT_BROWSER_VIEWPORT_WIDTH,
|
|
107
|
+
height: DEFAULT_BROWSER_VIEWPORT_HEIGHT,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
94
110
|
const waitUntil = (input.wait_until ??
|
|
95
111
|
this.config.browser.waitUntil);
|
|
96
112
|
await state.page.goto(targetUrl, {
|
|
@@ -108,13 +124,18 @@ class BrowserService {
|
|
|
108
124
|
createdContext,
|
|
109
125
|
waitUntil,
|
|
110
126
|
headless: this.config.browser.headless,
|
|
127
|
+
viewportWidth: state.page.viewportSize()?.width ?? requestedViewport?.width,
|
|
128
|
+
viewportHeight: state.page.viewportSize()?.height ?? requestedViewport?.height,
|
|
111
129
|
});
|
|
130
|
+
const viewport = state.page.viewportSize();
|
|
112
131
|
return {
|
|
113
132
|
session_id: normalizedSessionId,
|
|
114
133
|
opened: true,
|
|
115
134
|
created_context: createdContext,
|
|
116
135
|
url: state.currentUrl,
|
|
117
136
|
...(state.title ? { title: state.title } : {}),
|
|
137
|
+
...(viewport?.width ? { viewport_width: viewport.width } : {}),
|
|
138
|
+
...(viewport?.height ? { viewport_height: viewport.height } : {}),
|
|
118
139
|
};
|
|
119
140
|
}
|
|
120
141
|
async getConsole(input) {
|
|
@@ -690,10 +711,13 @@ class BrowserService {
|
|
|
690
711
|
async ensureBrowser() {
|
|
691
712
|
this.browserPromise ??= (async () => {
|
|
692
713
|
const playwright = await this.ensurePlaywright();
|
|
693
|
-
const launchArgs =
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
714
|
+
const launchArgs = [];
|
|
715
|
+
if (this.config.browser.headless === false) {
|
|
716
|
+
launchArgs.push("--start-maximized");
|
|
717
|
+
if (this.config.browser.devtools === true) {
|
|
718
|
+
launchArgs.push("--auto-open-devtools-for-tabs");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
697
721
|
const browser = await playwright.chromium.launch({
|
|
698
722
|
headless: this.config.browser.headless,
|
|
699
723
|
slowMo: this.config.browser.slowMoMs,
|
|
@@ -717,13 +741,13 @@ class BrowserService {
|
|
|
717
741
|
})();
|
|
718
742
|
return this.browserPromise;
|
|
719
743
|
}
|
|
720
|
-
async ensureSessionState(sessionId, forceNewContext) {
|
|
744
|
+
async ensureSessionState(sessionId, forceNewContext, openInput) {
|
|
721
745
|
const existing = this.sessionStates.get(sessionId);
|
|
722
746
|
if (existing && !forceNewContext) {
|
|
723
747
|
return { state: existing, createdContext: false };
|
|
724
748
|
}
|
|
725
749
|
const browser = await this.ensureBrowser();
|
|
726
|
-
const context = await browser.newContext();
|
|
750
|
+
const context = await browser.newContext(this.buildContextOptions(openInput));
|
|
727
751
|
const page = await context.newPage();
|
|
728
752
|
const createdAt = new Date().toISOString();
|
|
729
753
|
const state = {
|
|
@@ -763,6 +787,32 @@ class BrowserService {
|
|
|
763
787
|
this.sessionStates.set(sessionId, state);
|
|
764
788
|
return { state, createdContext: true };
|
|
765
789
|
}
|
|
790
|
+
resolveRequestedViewport(input) {
|
|
791
|
+
const width = input?.width;
|
|
792
|
+
const height = input?.height;
|
|
793
|
+
if (!width && !height) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
if (!width || !height) {
|
|
797
|
+
throw new Error("Browser viewport requires both width and height together.");
|
|
798
|
+
}
|
|
799
|
+
return { width, height };
|
|
800
|
+
}
|
|
801
|
+
buildContextOptions(input) {
|
|
802
|
+
const requestedViewport = this.resolveRequestedViewport(input);
|
|
803
|
+
if (requestedViewport) {
|
|
804
|
+
return {
|
|
805
|
+
viewport: requestedViewport,
|
|
806
|
+
screen: requestedViewport,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (this.config.browser.headless === false) {
|
|
810
|
+
return {
|
|
811
|
+
viewport: null,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
return {};
|
|
815
|
+
}
|
|
766
816
|
recordNetworkFailure(state, request, response) {
|
|
767
817
|
const failure = request.failure();
|
|
768
818
|
pushBounded(state.networkFailures, {
|
|
@@ -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: {
|
|
@@ -26,6 +26,9 @@ function principalActiveSessionKey(principal) {
|
|
|
26
26
|
function principalActiveSessionMatchPattern(telegramUserId) {
|
|
27
27
|
return `${KEY_PREFIX}:principal:*:${telegramUserId}:active-session`;
|
|
28
28
|
}
|
|
29
|
+
function bindingMatchPattern() {
|
|
30
|
+
return `${KEY_PREFIX}:binding:*`;
|
|
31
|
+
}
|
|
29
32
|
function menuPayloadKey(key) {
|
|
30
33
|
return `${KEY_PREFIX}:menu-payload:${key}`;
|
|
31
34
|
}
|
|
@@ -204,6 +207,31 @@ class RedisStateStore {
|
|
|
204
207
|
async listBoundSessionIdsForPrincipal(principal) {
|
|
205
208
|
return this.redis.smembers(principalSessionsKey(principal));
|
|
206
209
|
}
|
|
210
|
+
async listBoundPrincipals() {
|
|
211
|
+
const principals = new Map();
|
|
212
|
+
let cursor = "0";
|
|
213
|
+
do {
|
|
214
|
+
const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", bindingMatchPattern(), "COUNT", 100);
|
|
215
|
+
cursor = nextCursor;
|
|
216
|
+
for (const key of keys) {
|
|
217
|
+
const raw = await this.redis.get(key);
|
|
218
|
+
const binding = parseJson(raw);
|
|
219
|
+
if (!binding) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const telegramChatId = Number(binding.telegramChatId);
|
|
223
|
+
const telegramUserId = Number(binding.telegramUserId);
|
|
224
|
+
if (!Number.isFinite(telegramChatId) || !Number.isFinite(telegramUserId)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
principals.set(`${telegramChatId}:${telegramUserId}`, {
|
|
228
|
+
telegramChatId,
|
|
229
|
+
telegramUserId,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
} while (cursor !== "0");
|
|
233
|
+
return Array.from(principals.values());
|
|
234
|
+
}
|
|
207
235
|
async resetRuntimeState() {
|
|
208
236
|
await this.redis.del(activeRequestKey());
|
|
209
237
|
await this.redis.del(queueKey());
|
|
@@ -43,6 +43,7 @@ class TelegramTransport {
|
|
|
43
43
|
lifecycleActions;
|
|
44
44
|
attachmentStore;
|
|
45
45
|
broadcastActions;
|
|
46
|
+
consoleRegistry;
|
|
46
47
|
context;
|
|
47
48
|
documentActions;
|
|
48
49
|
eventActions;
|
|
@@ -185,6 +186,7 @@ class TelegramTransport {
|
|
|
185
186
|
this.lifecycleActions = composition.lifecycleActions;
|
|
186
187
|
this.attachmentStore = composition.attachmentStore;
|
|
187
188
|
this.broadcastActions = composition.broadcastActions;
|
|
189
|
+
this.consoleRegistry = composition.consoleRegistry;
|
|
188
190
|
this.context = composition.context;
|
|
189
191
|
this.documentActions = composition.documentActions;
|
|
190
192
|
this.eventActions = composition.eventActions;
|
|
@@ -436,9 +438,44 @@ class TelegramTransport {
|
|
|
436
438
|
async sendStartupNotifications() {
|
|
437
439
|
await this.lifecycleActions.sendStartupNotifications(__dirname);
|
|
438
440
|
}
|
|
441
|
+
ensurePromptScanRunning() {
|
|
442
|
+
this.terminalRuntime.ensurePromptScanRunning();
|
|
443
|
+
}
|
|
444
|
+
pausePromptScan() {
|
|
445
|
+
this.terminalRuntime.pausePromptScan();
|
|
446
|
+
}
|
|
439
447
|
async sendAdminGatewayRegistrationNotifications(input) {
|
|
440
448
|
await this.requestFlow.sendAdminGatewayRegistrationNotifications(input);
|
|
441
449
|
}
|
|
450
|
+
async hydrateGatewayClientOwnerRoute(input) {
|
|
451
|
+
if (this.config.distributed.mode !== "gateway") {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const route = await this.callGatewayJson("/user/route", {
|
|
455
|
+
client_uuid: input.clientUuid,
|
|
456
|
+
...(input.gatewayUserUuid ? { gateway_user_uuid: input.gatewayUserUuid } : {}),
|
|
457
|
+
});
|
|
458
|
+
if (typeof route.telegram_user_id !== "number" ||
|
|
459
|
+
typeof route.telegram_chat_id !== "number") {
|
|
460
|
+
this.logger.debug("Skipping gateway owner route hydration because Telegram route is unavailable", {
|
|
461
|
+
clientUuid: input.clientUuid,
|
|
462
|
+
gatewayUserUuid: input.gatewayUserUuid ?? null,
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
await this.consoleRegistry.ensureScopedConsolesBound({
|
|
467
|
+
principal: {
|
|
468
|
+
telegramChatId: route.telegram_chat_id,
|
|
469
|
+
telegramUserId: route.telegram_user_id,
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
this.logger.info("Gateway owner route hydrated for live client", {
|
|
473
|
+
clientUuid: input.clientUuid,
|
|
474
|
+
gatewayUserUuid: input.gatewayUserUuid ?? null,
|
|
475
|
+
telegramChatId: route.telegram_chat_id,
|
|
476
|
+
telegramUserId: route.telegram_user_id,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
442
479
|
async sendRequest(input) {
|
|
443
480
|
return this.requestFlow.sendRequest(input);
|
|
444
481
|
}
|
|
@@ -53,12 +53,18 @@ class TransportConsoleRegistry {
|
|
|
53
53
|
for (const consoleSession of consoles) {
|
|
54
54
|
const relaySessionId = (0, relay_1.buildLiveRelaySessionId)(consoleSession.client_uuid, consoleSession.local_session_id);
|
|
55
55
|
relaySessionIds.push(relaySessionId);
|
|
56
|
-
await this.materializeRelaySession(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
await this.materializeRelaySession(input.ctx
|
|
57
|
+
? {
|
|
58
|
+
ctx: input.ctx,
|
|
59
|
+
principal: input.principal,
|
|
60
|
+
relaySessionId,
|
|
61
|
+
consoleSession,
|
|
62
|
+
}
|
|
63
|
+
: {
|
|
64
|
+
principal: input.principal,
|
|
65
|
+
relaySessionId,
|
|
66
|
+
consoleSession,
|
|
67
|
+
});
|
|
62
68
|
}
|
|
63
69
|
const existingActive = await this.host.bindingStore.getActiveSessionIdForPrincipal(input.principal);
|
|
64
70
|
const nextBound = new Set(await this.host.bindingStore.listBoundSessionIdsForPrincipal(input.principal));
|
|
@@ -119,7 +125,7 @@ class TransportConsoleRegistry {
|
|
|
119
125
|
sessionId: input.relaySessionId,
|
|
120
126
|
telegramChatId: input.principal.telegramChatId,
|
|
121
127
|
telegramUserId: input.principal.telegramUserId,
|
|
122
|
-
...(input.ctx
|
|
128
|
+
...(input.ctx?.from?.username
|
|
123
129
|
? { telegramUsername: input.ctx.from.username }
|
|
124
130
|
: {}),
|
|
125
131
|
linkedAt: new Date().toISOString(),
|
|
@@ -92,6 +92,11 @@ function buildTransportConstructorWiring(host) {
|
|
|
92
92
|
resolveLocaleForTelegramUserId: (userId) => context.resolveLocaleForTelegramUserId(userId),
|
|
93
93
|
sendNotification: (input) => requestFlow.sendNotification(input),
|
|
94
94
|
sendLiveViewLauncherMessage: (input) => liveActions.sendLauncherMessage(input),
|
|
95
|
+
callGatewayJson: (path, payload) => host.callGatewayJson(path, payload),
|
|
96
|
+
createTerminalPromptActionPayload: (sessionId, actions) => payloadState.createTerminalPromptActionPayload(sessionId, actions),
|
|
97
|
+
getMenuPayloadByKey: (key) => host.menuPayloadStore.getMenuPayload(key),
|
|
98
|
+
resolveLocaleForContext: (ctx) => context.resolveLocaleForContext(ctx),
|
|
99
|
+
sendChatMessage: (telegramChatId, text, options, meta) => outputActions.sendChatMessage(telegramChatId, text, options, meta),
|
|
95
100
|
t: (locale, key, vars) => context.t(locale, key, vars),
|
|
96
101
|
});
|
|
97
102
|
const terminalRuntime = new transportTerminalRuntime_1.TransportTerminalRuntime({
|
|
@@ -103,6 +108,9 @@ function buildTransportConstructorWiring(host) {
|
|
|
103
108
|
isTelegramEnabled: () => host.config.distributed.mode !== "client" && Boolean(host.config.telegram.botToken?.trim()),
|
|
104
109
|
terminalActions,
|
|
105
110
|
terminalNudgeDebounceTimers: host.terminalNudgeDebounceTimers,
|
|
111
|
+
ensureGatewayScopedConsolesBoundForPrincipal: async (principal) => {
|
|
112
|
+
await consoleRegistry.ensureScopedConsolesBound({ principal });
|
|
113
|
+
},
|
|
106
114
|
});
|
|
107
115
|
const menuFingerprints = new transportMenuFingerprints_1.TransportMenuFingerprints({
|
|
108
116
|
logger: host.logger,
|
|
@@ -560,6 +568,7 @@ function buildTransportConstructorWiring(host) {
|
|
|
560
568
|
handleProjectMemberNoteCallback: (ctx) => projectActions.handleProjectMemberNoteCallback(ctx),
|
|
561
569
|
handleProjectMemberLiveCallback: (ctx) => projectActions.handleProjectMemberLiveCallback(ctx),
|
|
562
570
|
handleLiveApprovalCallback: (ctx) => projectActions.handleLiveApprovalCallback(ctx),
|
|
571
|
+
handleTerminalPromptActionCallback: (ctx) => terminalActions.handlePromptActionCallback(ctx),
|
|
563
572
|
handleProjectDetailCallback: (ctx) => projectActions.handleProjectDetailCallback(ctx),
|
|
564
573
|
handleProjectDeleteCallback: (ctx) => projectActions.handleProjectDeleteCallback(ctx),
|
|
565
574
|
handleProjectLeaveCallback: (ctx) => projectActions.handleProjectLeaveCallback(ctx),
|
package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMenuShell.js
CHANGED
|
@@ -41,6 +41,9 @@ class TransportMenuShell {
|
|
|
41
41
|
bot.callbackQuery(/^live-approval:(approve|deny):(.+)$/u, async (ctx) => {
|
|
42
42
|
await this.host.handleLiveApprovalCallback(ctx);
|
|
43
43
|
});
|
|
44
|
+
bot.callbackQuery(/^terminal-prompt-action:(\d+|enter|escape):(.+)$/u, async (ctx) => {
|
|
45
|
+
await this.host.handleTerminalPromptActionCallback(ctx);
|
|
46
|
+
});
|
|
44
47
|
bot.callbackQuery(/^project-detail:(.+)$/u, async (ctx) => {
|
|
45
48
|
await this.host.handleProjectDetailCallback(ctx);
|
|
46
49
|
});
|
|
@@ -77,6 +77,13 @@ class TransportPayloadState {
|
|
|
77
77
|
...(input.projectName ? { projectName: input.projectName } : {}),
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
async createTerminalPromptActionPayload(sessionId, actions) {
|
|
81
|
+
return this.createPayload({
|
|
82
|
+
kind: "terminal-prompt-action",
|
|
83
|
+
sessionId,
|
|
84
|
+
promptActions: actions,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
80
87
|
async createPartnerFileTargetPayload(sessionId, targetSessionId, title, filePath) {
|
|
81
88
|
return this.createPayload({
|
|
82
89
|
kind: "partner-file-target",
|