@deadragdoll/tellymcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example.client +93 -0
- package/.env.example.gateway +120 -0
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README-ru.md +338 -0
- package/README.md +1262 -0
- package/STANDALONE-ru.md +266 -0
- package/STANDALONE.md +266 -0
- package/TOOLS.md +1296 -0
- package/config/templates/env.both.template +83 -0
- package/config/templates/env.client.template +60 -0
- package/config/templates/env.gateway.template +82 -0
- package/dist/cli.js +636 -0
- package/dist/index.js +17 -0
- package/dist/lib/logfeed/store.js +52 -0
- package/dist/lib/middlewares/tracer.js +172 -0
- package/dist/lib/mixins/db.js +267 -0
- package/dist/lib/mixins/logfeed.js +34 -0
- package/dist/lib/mixins/session.errors.js +142 -0
- package/dist/lib/moleculer.js +2 -0
- package/dist/lib/trace.js +147 -0
- package/dist/lib/traceContext.js +116 -0
- package/dist/moleculer.config.js +274 -0
- package/dist/services/features/telegram-mcp/approval.service.js +33 -0
- package/dist/services/features/telegram-mcp/browser.service.js +42 -0
- package/dist/services/features/telegram-mcp/collaboration.service.js +53 -0
- package/dist/services/features/telegram-mcp/ensuredb.service.js +337 -0
- package/dist/services/features/telegram-mcp/gateway-delivery.service.js +378 -0
- package/dist/services/features/telegram-mcp/gateway-loopback.js +10 -0
- package/dist/services/features/telegram-mcp/gateway-rmq.service.js +294 -0
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +1463 -0
- package/dist/services/features/telegram-mcp/gateway.service.js +1141 -0
- package/dist/services/features/telegram-mcp/inbox.service.js +33 -0
- package/dist/services/features/telegram-mcp/mcp-http.service.js +76 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +127 -0
- package/dist/services/features/telegram-mcp/notify.service.js +33 -0
- package/dist/services/features/telegram-mcp/pair.service.js +33 -0
- package/dist/services/features/telegram-mcp/runtime.service.js +36 -0
- package/dist/services/features/telegram-mcp/session-context.service.js +33 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +103 -0
- package/dist/services/features/telegram-mcp/src/app/config/env.js +317 -0
- package/dist/services/features/telegram-mcp/src/app/http.js +774 -0
- package/dist/services/features/telegram-mcp/src/app/index.js +2 -0
- package/dist/services/features/telegram-mcp/src/app/providers/mcp/server.js +13 -0
- package/dist/services/features/telegram-mcp/src/app/providers/redis/client.js +18 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +740 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +267 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/relay.js +69 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/tmux.js +9 -0
- package/dist/services/features/telegram-mcp/src/entities/auth/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/browser/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/collaboration/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/inbox/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +545 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/entities/session/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/ask-user/model/askUserTelegram.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClearLogsTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserCloseTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserComputedStyleTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserConsoleTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserErrorsTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserNetworkFailuresTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserOpenTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserReloadTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +689 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserWaitForTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserWaitForUrlTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/backend.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +26 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/localCollaborationBackend.js +390 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +102 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerNoteTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayCollaborationBackend.js +69 -0
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +657 -0
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayReplyResolution.js +17 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/deleteTelegramInboxMessageTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/getTelegramInboxCountTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/getTelegramInboxTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/inbox/model/inboxService.js +77 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +93 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyTelegramTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/pair-session/model/clearSessionPairingTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/pair-session/model/createSessionPairCodeTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/pair-session/model/generatePairCode.js +202 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/clearSessionContextTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/getSessionContextTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/getTmuxTargetTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/renameSessionTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/sessionContextService.js +409 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/setSessionContextTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/setTmuxTargetTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/tools-sync/model/refreshToolsMarkdownService.js +123 -0
- package/dist/services/features/telegram-mcp/src/features/tools-sync/model/refreshToolsMarkdownTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/processes/human-approval/model/orchestrator.js +243 -0
- package/dist/services/features/telegram-mcp/src/shared/api/storage/contract.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/api/tool-registry/registry.js +8 -0
- package/dist/services/features/telegram-mcp/src/shared/api/tool-registry/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/api/transport/contract.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/object-storage/minioExchangeStore.js +86 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +436 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/collabSemantics.js +21 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/collabUi.js +87 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/messageFormat.js +60 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/proxyFetch.js +46 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +6534 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/tmux/client.js +280 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/ids/ids.js +34 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/logger/logger.js +68 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/project-identity/projectIdentity.js +223 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/redact-secrets/redactSecrets.js +22 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/truncate/truncate.js +12 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/version/versionHandshake.js +124 -0
- package/dist/services/features/telegram-mcp/src/shared/types/common.js +2 -0
- package/dist/services/features/telegram-mcp/standalone-http.service.js +113 -0
- package/dist/services/features/telegram-mcp/tools-sync.service.js +33 -0
- package/package.json +110 -0
- package/scripts/postinstall.js +60 -0
|
@@ -0,0 +1,1463 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TELEGRAM_MCP_GATEWAY_SOCKET_SERVICE_NAME = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const runtime_service_1 = require("./runtime.service");
|
|
8
|
+
const standalone_http_service_1 = require("./standalone-http.service");
|
|
9
|
+
const auth_1 = require("./src/app/webapp/auth");
|
|
10
|
+
const tmux_1 = require("./src/app/webapp/tmux");
|
|
11
|
+
const gateway_loopback_1 = require("./gateway-loopback");
|
|
12
|
+
const versionHandshake_1 = require("./src/shared/lib/version/versionHandshake");
|
|
13
|
+
const wsLib = require("ws");
|
|
14
|
+
const WebSocketServer = wsLib.WebSocketServer;
|
|
15
|
+
exports.TELEGRAM_MCP_GATEWAY_SOCKET_SERVICE_NAME = "telegramMcp.gatewaySocket";
|
|
16
|
+
const CLIENT_RECONNECT_DELAY_MS = 3000;
|
|
17
|
+
const LIVE_REQUEST_TIMEOUT_MS = 20000;
|
|
18
|
+
const TOOLS_SYNC_CHECK_INTERVAL_MS = 15000;
|
|
19
|
+
const WS_HEARTBEAT_INTERVAL_MS = 10000;
|
|
20
|
+
function normalizeWebSocketUrl(value, defaultPath) {
|
|
21
|
+
const url = new URL(value);
|
|
22
|
+
if (url.protocol === "https:") {
|
|
23
|
+
url.protocol = "wss:";
|
|
24
|
+
}
|
|
25
|
+
else if (url.protocol === "http:") {
|
|
26
|
+
url.protocol = "ws:";
|
|
27
|
+
}
|
|
28
|
+
if (!url.pathname || url.pathname === "/") {
|
|
29
|
+
url.pathname = defaultPath;
|
|
30
|
+
}
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
function formatTmuxRelayError(error) {
|
|
34
|
+
if ((0, tmux_1.isTmuxUnavailableError)(error)) {
|
|
35
|
+
return "tmux is unavailable";
|
|
36
|
+
}
|
|
37
|
+
return error instanceof Error ? error.message : String(error);
|
|
38
|
+
}
|
|
39
|
+
function computeToolsHashForDir(workspaceDir) {
|
|
40
|
+
const toolsPath = (0, node_path_1.join)((0, node_path_1.resolve)(workspaceDir), "TOOLS.md");
|
|
41
|
+
if (!(0, node_fs_1.existsSync)(toolsPath)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const content = (0, node_fs_1.readFileSync)(toolsPath, "utf8");
|
|
45
|
+
return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function normalizeGatewayBaseUrl(value) {
|
|
48
|
+
const url = new URL(value);
|
|
49
|
+
url.pathname = url.pathname.replace(/\/+$/u, "");
|
|
50
|
+
if (!url.pathname.endsWith("/gateway")) {
|
|
51
|
+
url.pathname = `${url.pathname}/gateway`.replace(/\/{2,}/gu, "/");
|
|
52
|
+
}
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
const TelegramMcpGatewaySocketService = {
|
|
56
|
+
name: exports.TELEGRAM_MCP_GATEWAY_SOCKET_SERVICE_NAME,
|
|
57
|
+
dependencies: [
|
|
58
|
+
runtime_service_1.TELEGRAM_MCP_RUNTIME_SERVICE_NAME,
|
|
59
|
+
standalone_http_service_1.TELEGRAM_MCP_STANDALONE_HTTP_SERVICE_NAME,
|
|
60
|
+
],
|
|
61
|
+
actions: {
|
|
62
|
+
requestLiveRelay: {
|
|
63
|
+
params: {
|
|
64
|
+
clientUuid: "string",
|
|
65
|
+
localSessionId: "string",
|
|
66
|
+
requestType: {
|
|
67
|
+
type: "enum",
|
|
68
|
+
values: ["bootstrap", "bootstrap_validate", "view", "action"],
|
|
69
|
+
},
|
|
70
|
+
payload: { type: "object", optional: true },
|
|
71
|
+
},
|
|
72
|
+
async handler(ctx) {
|
|
73
|
+
return await this.requestLiveRelay?.({
|
|
74
|
+
clientUuid: ctx.params.clientUuid,
|
|
75
|
+
localSessionId: ctx.params.localSessionId,
|
|
76
|
+
requestType: ctx.params.requestType,
|
|
77
|
+
payload: ctx.params.payload ?? {},
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
notifyProjectMemberJoined: {
|
|
82
|
+
params: {
|
|
83
|
+
clientUuids: { type: "array", items: "string" },
|
|
84
|
+
projectUuid: "string",
|
|
85
|
+
projectName: "string",
|
|
86
|
+
memberDisplayName: { type: "string", optional: true },
|
|
87
|
+
memberTelegramUsername: { type: "string", optional: true },
|
|
88
|
+
},
|
|
89
|
+
async handler(ctx) {
|
|
90
|
+
return await this.notifyProjectMemberJoined?.(ctx.params);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
notifyProjectMemberLeft: {
|
|
94
|
+
params: {
|
|
95
|
+
clientUuids: { type: "array", items: "string" },
|
|
96
|
+
projectUuid: "string",
|
|
97
|
+
projectName: "string",
|
|
98
|
+
memberDisplayName: { type: "string", optional: true },
|
|
99
|
+
memberTelegramUsername: { type: "string", optional: true },
|
|
100
|
+
},
|
|
101
|
+
async handler(ctx) {
|
|
102
|
+
return await this.notifyProjectMemberLeft?.(ctx.params);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
notifyProjectDeleted: {
|
|
106
|
+
params: {
|
|
107
|
+
clientUuids: { type: "array", items: "string" },
|
|
108
|
+
projectUuid: "string",
|
|
109
|
+
projectName: "string",
|
|
110
|
+
},
|
|
111
|
+
async handler(ctx) {
|
|
112
|
+
return await this.notifyProjectDeleted?.(ctx.params);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
notifyLiveApprovalRequest: {
|
|
116
|
+
params: {
|
|
117
|
+
clientUuid: "string",
|
|
118
|
+
payload: { type: "object" },
|
|
119
|
+
},
|
|
120
|
+
async handler(ctx) {
|
|
121
|
+
return {
|
|
122
|
+
delivered: await this.notifyLiveApprovalRequest?.(ctx.params),
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
notifyLiveApprovalResolved: {
|
|
127
|
+
params: {
|
|
128
|
+
clientUuid: "string",
|
|
129
|
+
approved: "boolean",
|
|
130
|
+
payload: { type: "object" },
|
|
131
|
+
},
|
|
132
|
+
async handler(ctx) {
|
|
133
|
+
return {
|
|
134
|
+
delivered: await this.notifyLiveApprovalResolved?.(ctx.params),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
notifyDeliveryQueued: {
|
|
139
|
+
params: {
|
|
140
|
+
clientUuid: "string",
|
|
141
|
+
delivery: { type: "object" },
|
|
142
|
+
},
|
|
143
|
+
async handler(ctx) {
|
|
144
|
+
return await this.notifyDeliveryQueued?.(ctx.params);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
notifyDeliveryStatus: {
|
|
148
|
+
params: {
|
|
149
|
+
clientUuid: "string",
|
|
150
|
+
status: { type: "object" },
|
|
151
|
+
},
|
|
152
|
+
async handler(ctx) {
|
|
153
|
+
return await this.notifyDeliveryStatus?.(ctx.params);
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
created() {
|
|
158
|
+
this.runtimeService = null;
|
|
159
|
+
this.standaloneHttpService = null;
|
|
160
|
+
this.wsServer = null;
|
|
161
|
+
this.wsClient = null;
|
|
162
|
+
this.wsReconnectTimer = null;
|
|
163
|
+
this.wsIdentityRefreshTimer = null;
|
|
164
|
+
this.wsToolsSyncTimer = null;
|
|
165
|
+
this.wsHeartbeatTimer = null;
|
|
166
|
+
this.wsAwaitingPong = false;
|
|
167
|
+
this.wsConnectionId = null;
|
|
168
|
+
this.wsHelloClientUuid = null;
|
|
169
|
+
this.wsHelloSessionToolsSnapshot = null;
|
|
170
|
+
this.wsClientHasConnectedOnce = false;
|
|
171
|
+
this.wsUpgradeHandler = null;
|
|
172
|
+
this.stopRequested = false;
|
|
173
|
+
this.connectedClients = new Map();
|
|
174
|
+
this.connectedClientsByUuid = new Map();
|
|
175
|
+
this.connectedClientToolsAlerts = new Map();
|
|
176
|
+
this.pendingLiveRequests = new Map();
|
|
177
|
+
},
|
|
178
|
+
methods: {
|
|
179
|
+
getRuntimeOrThrow() {
|
|
180
|
+
const runtimeService = this.runtimeService ??
|
|
181
|
+
this.broker.getLocalService(runtime_service_1.TELEGRAM_MCP_RUNTIME_SERVICE_NAME);
|
|
182
|
+
if (!runtimeService) {
|
|
183
|
+
throw new Error(`Local Moleculer service '${runtime_service_1.TELEGRAM_MCP_RUNTIME_SERVICE_NAME}' is unavailable`);
|
|
184
|
+
}
|
|
185
|
+
this.runtimeService = runtimeService;
|
|
186
|
+
return runtimeService.getRuntime();
|
|
187
|
+
},
|
|
188
|
+
getHttpServerOrThrow() {
|
|
189
|
+
const standaloneHttpService = this.standaloneHttpService ??
|
|
190
|
+
this.broker.getLocalService(standalone_http_service_1.TELEGRAM_MCP_STANDALONE_HTTP_SERVICE_NAME);
|
|
191
|
+
if (!standaloneHttpService?.httpServer) {
|
|
192
|
+
throw new Error(`Local Moleculer service '${standalone_http_service_1.TELEGRAM_MCP_STANDALONE_HTTP_SERVICE_NAME}' HTTP server is unavailable`);
|
|
193
|
+
}
|
|
194
|
+
this.standaloneHttpService = standaloneHttpService;
|
|
195
|
+
return standaloneHttpService.httpServer;
|
|
196
|
+
},
|
|
197
|
+
async collectSessionTools() {
|
|
198
|
+
const runtime = this.getRuntimeOrThrow();
|
|
199
|
+
const sessions = await runtime.sessionStore.listSessions();
|
|
200
|
+
const boundSessions = (await Promise.all(sessions.map(async (session) => (await runtime.bindingStore.getBinding(session.sessionId))
|
|
201
|
+
? session
|
|
202
|
+
: null))).filter((session) => Boolean(session));
|
|
203
|
+
const sessionTools = boundSessions
|
|
204
|
+
.map((session) => {
|
|
205
|
+
const toolsHash = session.cwd
|
|
206
|
+
? computeToolsHashForDir(session.cwd)
|
|
207
|
+
: null;
|
|
208
|
+
return {
|
|
209
|
+
local_session_id: session.sessionId,
|
|
210
|
+
...(session.label ? { session_label: session.label } : {}),
|
|
211
|
+
...(toolsHash ? { tools_hash: toolsHash } : {}),
|
|
212
|
+
};
|
|
213
|
+
})
|
|
214
|
+
.sort((left, right) => left.local_session_id.localeCompare(right.local_session_id));
|
|
215
|
+
return {
|
|
216
|
+
sessionTools,
|
|
217
|
+
snapshot: JSON.stringify(sessionTools),
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
getGatewayToolsHash() {
|
|
221
|
+
return computeToolsHashForDir(process.cwd());
|
|
222
|
+
},
|
|
223
|
+
getLocalVersionInfo() {
|
|
224
|
+
return {
|
|
225
|
+
packageVersion: (0, versionHandshake_1.getTellyMcpPackageVersion)(__dirname),
|
|
226
|
+
protocolVersion: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION,
|
|
227
|
+
capabilities: [...versionHandshake_1.TELLYMCP_CAPABILITIES],
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
async fetchGatewayToolsHashForClient() {
|
|
231
|
+
const runtime = this.getRuntimeOrThrow();
|
|
232
|
+
if (runtime.config.distributed.mode === "gateway" ||
|
|
233
|
+
runtime.config.distributed.mode === "both") {
|
|
234
|
+
return this.getGatewayToolsHash?.() ?? null;
|
|
235
|
+
}
|
|
236
|
+
const gatewayPublicUrl = runtime.config.distributed.gatewayPublicUrl;
|
|
237
|
+
if (!gatewayPublicUrl) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const url = normalizeGatewayBaseUrl(gatewayPublicUrl);
|
|
241
|
+
url.pathname = `${url.pathname}/tools-md`.replace(/\/{2,}/gu, "/");
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(url, {
|
|
244
|
+
method: "GET",
|
|
245
|
+
headers: {
|
|
246
|
+
...(runtime.config.distributed.gatewayAuthToken
|
|
247
|
+
? { authorization: `Bearer ${runtime.config.distributed.gatewayAuthToken}` }
|
|
248
|
+
: {}),
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
const message = await response.text();
|
|
253
|
+
throw new Error(`Gateway TOOLS.md request failed with status ${response.status}: ${message || response.statusText}`);
|
|
254
|
+
}
|
|
255
|
+
return (0, node_crypto_1.createHash)("sha256").update(await response.text()).digest("hex");
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
runtime.logger.debug("Gateway TOOLS.md self-check skipped", {
|
|
259
|
+
gatewayPublicUrl,
|
|
260
|
+
error: error instanceof Error ? error.message : String(error),
|
|
261
|
+
});
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
async syncLocalToolsAgainstGateway(sessionId) {
|
|
266
|
+
const runtime = this.getRuntimeOrThrow();
|
|
267
|
+
const gatewayToolsHash = await this.fetchGatewayToolsHashForClient?.();
|
|
268
|
+
if (!gatewayToolsHash) {
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
const sessions = sessionId
|
|
272
|
+
? [await runtime.sessionStore.getSession(sessionId)].filter((item) => Boolean(item))
|
|
273
|
+
: await runtime.sessionStore.listSessions();
|
|
274
|
+
const boundSessions = (await Promise.all(sessions.map(async (session) => (await runtime.bindingStore.getBinding(session.sessionId))
|
|
275
|
+
? session
|
|
276
|
+
: null))).filter((session) => Boolean(session));
|
|
277
|
+
let delivered = 0;
|
|
278
|
+
for (const session of boundSessions) {
|
|
279
|
+
const localHash = session.cwd
|
|
280
|
+
? computeToolsHashForDir(session.cwd)
|
|
281
|
+
: null;
|
|
282
|
+
if (localHash === gatewayToolsHash) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (session.lastSeenToolsHash?.trim() === gatewayToolsHash ||
|
|
286
|
+
session.lastNotifiedToolsHash?.trim() === gatewayToolsHash) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
await runtime.telegramTransport.handleToolsUpdatedEvent({
|
|
290
|
+
local_session_id: session.sessionId,
|
|
291
|
+
...(session.label ? { session_label: session.label } : {}),
|
|
292
|
+
...(localHash ? { client_tools_hash: localHash } : {}),
|
|
293
|
+
gateway_tools_hash: gatewayToolsHash,
|
|
294
|
+
reason: localHash ? "outdated" : "missing",
|
|
295
|
+
instruction: "Call refresh_tools_markdown for this session, then re-read the local TOOLS.md and apply it before continuing.",
|
|
296
|
+
});
|
|
297
|
+
delivered += 1;
|
|
298
|
+
}
|
|
299
|
+
return delivered;
|
|
300
|
+
},
|
|
301
|
+
async notifyToolsMismatchForSocket(socket, hello) {
|
|
302
|
+
const gatewayToolsHash = this.getGatewayToolsHash?.();
|
|
303
|
+
if (!gatewayToolsHash || !socket || socket.readyState !== 1) {
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
const sessionTools = Array.isArray(hello.session_tools)
|
|
307
|
+
? hello.session_tools
|
|
308
|
+
: [];
|
|
309
|
+
if (sessionTools.length === 0) {
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
const alerted = this.connectedClientToolsAlerts?.get(socket) ?? new Map();
|
|
313
|
+
this.connectedClientToolsAlerts?.set(socket, alerted);
|
|
314
|
+
let delivered = 0;
|
|
315
|
+
for (const sessionTool of sessionTools) {
|
|
316
|
+
const clientToolsHash = typeof sessionTool.tools_hash === "string" && sessionTool.tools_hash.trim()
|
|
317
|
+
? sessionTool.tools_hash.trim()
|
|
318
|
+
: null;
|
|
319
|
+
if (clientToolsHash === gatewayToolsHash) {
|
|
320
|
+
alerted.delete(sessionTool.local_session_id);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (alerted.get(sessionTool.local_session_id) === gatewayToolsHash) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
socket.send(JSON.stringify({
|
|
327
|
+
type: "tools_event",
|
|
328
|
+
payload: {
|
|
329
|
+
local_session_id: sessionTool.local_session_id,
|
|
330
|
+
...(sessionTool.session_label
|
|
331
|
+
? { session_label: sessionTool.session_label }
|
|
332
|
+
: {}),
|
|
333
|
+
...(clientToolsHash ? { client_tools_hash: clientToolsHash } : {}),
|
|
334
|
+
gateway_tools_hash: gatewayToolsHash,
|
|
335
|
+
reason: clientToolsHash ? "outdated" : "missing",
|
|
336
|
+
instruction: "Call refresh_tools_markdown for this session, then re-read the local TOOLS.md and apply it before continuing.",
|
|
337
|
+
},
|
|
338
|
+
}));
|
|
339
|
+
alerted.set(sessionTool.local_session_id, gatewayToolsHash);
|
|
340
|
+
delivered += 1;
|
|
341
|
+
}
|
|
342
|
+
return delivered;
|
|
343
|
+
},
|
|
344
|
+
async sendClientHello(socket) {
|
|
345
|
+
const runtime = this.getRuntimeOrThrow();
|
|
346
|
+
if (!runtime || !socket || socket.readyState !== 1) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const clientUuid = await runtime.maintenanceStore.getGatewayClientUuid();
|
|
350
|
+
const { sessionTools, snapshot } = await this.collectSessionTools?.() ?? {
|
|
351
|
+
sessionTools: [],
|
|
352
|
+
snapshot: "[]",
|
|
353
|
+
};
|
|
354
|
+
const versionInfo = this.getLocalVersionInfo?.() ?? {
|
|
355
|
+
packageVersion: "0.0.0-unknown",
|
|
356
|
+
protocolVersion: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION,
|
|
357
|
+
capabilities: [...versionHandshake_1.TELLYMCP_CAPABILITIES],
|
|
358
|
+
};
|
|
359
|
+
const hello = {
|
|
360
|
+
type: "hello",
|
|
361
|
+
connection_id: this.wsConnectionId || (0, node_crypto_1.randomUUID)(),
|
|
362
|
+
role: "client",
|
|
363
|
+
...(clientUuid ? { client_uuid: clientUuid } : {}),
|
|
364
|
+
...(runtime.config.project.name
|
|
365
|
+
? { project_name: runtime.config.project.name }
|
|
366
|
+
: {}),
|
|
367
|
+
...(this.broker.nodeID ? { node_id: this.broker.nodeID } : {}),
|
|
368
|
+
package_version: versionInfo.packageVersion,
|
|
369
|
+
protocol_version: versionInfo.protocolVersion,
|
|
370
|
+
capabilities: versionInfo.capabilities,
|
|
371
|
+
...(sessionTools.length > 0 ? { session_tools: sessionTools } : {}),
|
|
372
|
+
};
|
|
373
|
+
this.wsHelloClientUuid = clientUuid ?? null;
|
|
374
|
+
this.wsHelloSessionTools = sessionTools;
|
|
375
|
+
this.wsHelloSessionToolsSnapshot = snapshot;
|
|
376
|
+
socket.send(JSON.stringify(hello));
|
|
377
|
+
},
|
|
378
|
+
async isLocalGatewayClientUuid(clientUuid) {
|
|
379
|
+
const runtime = this.getRuntimeOrThrow();
|
|
380
|
+
const localClientUuid = await runtime.maintenanceStore.getGatewayClientUuid();
|
|
381
|
+
return Boolean(localClientUuid && localClientUuid === clientUuid);
|
|
382
|
+
},
|
|
383
|
+
async handleLocalIncomingDelivery(params) {
|
|
384
|
+
const runtime = this.getRuntimeOrThrow();
|
|
385
|
+
const localTargetSession = await runtime.sessionStore.getSession(params.delivery.target_local_session_id);
|
|
386
|
+
if (!(0, gateway_loopback_1.hasLocalTargetSession)(localTargetSession)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
await this.broker.call("telegramMcp.gatewayDelivery.materializeIncomingDelivery", {
|
|
391
|
+
delivery: params.delivery,
|
|
392
|
+
}, { meta: { internal_call: true } });
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
const result = await this.broker.call("telegramMcp.gateway.failDeliveries", {
|
|
396
|
+
client_uuid: params.clientUuid,
|
|
397
|
+
delivery_ids: [params.delivery.delivery_uuid],
|
|
398
|
+
error_text: error instanceof Error ? error.message : String(error),
|
|
399
|
+
}, { meta: { internal_call: true } });
|
|
400
|
+
for (const status of Array.isArray(result.deliveries)
|
|
401
|
+
? result.deliveries
|
|
402
|
+
: []) {
|
|
403
|
+
if (!status.source_client_uuid) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const published = await this.notifyDeliveryStatus?.({
|
|
407
|
+
clientUuid: status.source_client_uuid,
|
|
408
|
+
status,
|
|
409
|
+
});
|
|
410
|
+
if (!published && (await this.isLocalGatewayClientUuid?.(status.source_client_uuid))) {
|
|
411
|
+
await this.handleLocalDeliveryStatus?.({
|
|
412
|
+
clientUuid: status.source_client_uuid,
|
|
413
|
+
status,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
const result = await this.broker.call("telegramMcp.gateway.ackDeliveries", {
|
|
420
|
+
client_uuid: params.clientUuid,
|
|
421
|
+
delivery_ids: [params.delivery.delivery_uuid],
|
|
422
|
+
}, { meta: { internal_call: true } });
|
|
423
|
+
for (const status of Array.isArray(result.deliveries)
|
|
424
|
+
? result.deliveries
|
|
425
|
+
: []) {
|
|
426
|
+
if (!status.source_client_uuid) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const published = await this.notifyDeliveryStatus?.({
|
|
430
|
+
clientUuid: status.source_client_uuid,
|
|
431
|
+
status,
|
|
432
|
+
});
|
|
433
|
+
if (!published && (await this.isLocalGatewayClientUuid?.(status.source_client_uuid))) {
|
|
434
|
+
await this.handleLocalDeliveryStatus?.({
|
|
435
|
+
clientUuid: status.source_client_uuid,
|
|
436
|
+
status,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
async handleLocalDeliveryStatus(params) {
|
|
443
|
+
const runtime = this.getRuntimeOrThrow();
|
|
444
|
+
const notices = await runtime.maintenanceStore.listOutgoingDeliveryNotices();
|
|
445
|
+
if (!(0, gateway_loopback_1.hasOutgoingDeliveryNotice)(notices, params.status.delivery_uuid)) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
await this.broker.call("telegramMcp.gatewayDelivery.applyOutgoingDeliveryStatus", {
|
|
449
|
+
status: params.status,
|
|
450
|
+
}, { meta: { internal_call: true } });
|
|
451
|
+
return true;
|
|
452
|
+
},
|
|
453
|
+
async processLiveRequest(request) {
|
|
454
|
+
const runtime = this.getRuntimeOrThrow();
|
|
455
|
+
try {
|
|
456
|
+
if (request.request_type === "bootstrap_validate") {
|
|
457
|
+
const payload = request.payload ?? {};
|
|
458
|
+
const initDataRaw = typeof payload.initDataRaw === "string" ? payload.initDataRaw : "";
|
|
459
|
+
const initDataUnsafe = payload.initDataUnsafe && typeof payload.initDataUnsafe === "object"
|
|
460
|
+
? payload.initDataUnsafe
|
|
461
|
+
: null;
|
|
462
|
+
if (!initDataRaw || !initDataUnsafe) {
|
|
463
|
+
throw new Error("initDataRaw and initDataUnsafe are required");
|
|
464
|
+
}
|
|
465
|
+
const validated = (0, auth_1.validateTelegramWebAppInitData)(initDataRaw, initDataUnsafe, runtime.config.telegram.botToken, runtime.config.webapp.initDataTtlSeconds);
|
|
466
|
+
const launchRecord = runtime.webAppLaunchRegistry.getByUserId(validated.user.id);
|
|
467
|
+
if (!launchRecord) {
|
|
468
|
+
throw new Error("No pending Telegram WebApp launch was found");
|
|
469
|
+
}
|
|
470
|
+
runtime.webAppLaunchRegistry.deleteByUserId(validated.user.id);
|
|
471
|
+
if (launchRecord?.telegramChatId !== undefined &&
|
|
472
|
+
launchRecord?.telegramMessageId !== undefined) {
|
|
473
|
+
void runtime.telegramTransport
|
|
474
|
+
.deleteMessage(launchRecord.telegramChatId, launchRecord.telegramMessageId)
|
|
475
|
+
.catch(() => undefined);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
type: "live_response",
|
|
479
|
+
request_id: request.request_id,
|
|
480
|
+
ok: true,
|
|
481
|
+
result: {
|
|
482
|
+
telegram_user_id: validated.user.id,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (request.request_type === "bootstrap") {
|
|
487
|
+
const payload = request.payload ?? {};
|
|
488
|
+
const allowForeignBinding = payload.allowForeignBinding === true;
|
|
489
|
+
const trustedTelegramUserId = typeof payload.telegramUserId === "number"
|
|
490
|
+
? payload.telegramUserId
|
|
491
|
+
: typeof payload.telegramUserId === "string" &&
|
|
492
|
+
payload.telegramUserId.trim()
|
|
493
|
+
? Number(payload.telegramUserId)
|
|
494
|
+
: null;
|
|
495
|
+
let telegramUserId = trustedTelegramUserId;
|
|
496
|
+
if (telegramUserId !== null &&
|
|
497
|
+
(!Number.isFinite(telegramUserId) || telegramUserId <= 0)) {
|
|
498
|
+
telegramUserId = null;
|
|
499
|
+
}
|
|
500
|
+
if (telegramUserId === null) {
|
|
501
|
+
const initDataRaw = typeof payload.initDataRaw === "string" ? payload.initDataRaw : "";
|
|
502
|
+
const initDataUnsafe = payload.initDataUnsafe && typeof payload.initDataUnsafe === "object"
|
|
503
|
+
? payload.initDataUnsafe
|
|
504
|
+
: null;
|
|
505
|
+
if (!initDataRaw || !initDataUnsafe) {
|
|
506
|
+
throw new Error("initDataRaw and initDataUnsafe are required");
|
|
507
|
+
}
|
|
508
|
+
const validated = (0, auth_1.validateTelegramWebAppInitData)(initDataRaw, initDataUnsafe, runtime.config.telegram.botToken, runtime.config.webapp.initDataTtlSeconds);
|
|
509
|
+
telegramUserId = validated.user.id;
|
|
510
|
+
}
|
|
511
|
+
const sessionId = request.local_session_id.trim();
|
|
512
|
+
if (!sessionId) {
|
|
513
|
+
throw new Error("sessionId is missing for relay bootstrap");
|
|
514
|
+
}
|
|
515
|
+
const binding = await runtime.bindingStore.getBinding(sessionId);
|
|
516
|
+
if (!allowForeignBinding &&
|
|
517
|
+
(!binding || binding.telegramUserId !== telegramUserId)) {
|
|
518
|
+
throw new Error("This Telegram user is not bound to the requested session.");
|
|
519
|
+
}
|
|
520
|
+
const session = await runtime.sessionStore.getSession(sessionId);
|
|
521
|
+
return {
|
|
522
|
+
type: "live_response",
|
|
523
|
+
request_id: request.request_id,
|
|
524
|
+
ok: true,
|
|
525
|
+
result: {
|
|
526
|
+
session_id: sessionId,
|
|
527
|
+
session_label: session?.label ?? null,
|
|
528
|
+
tmux_target: Boolean(session?.tmuxTarget),
|
|
529
|
+
poll_interval_ms: runtime.config.webapp.pollIntervalMs,
|
|
530
|
+
telegram_user_id: telegramUserId,
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (request.request_type === "view") {
|
|
535
|
+
const sessionId = request.local_session_id.trim();
|
|
536
|
+
const session = await runtime.sessionStore.getSession(sessionId);
|
|
537
|
+
if (!session?.tmuxTarget) {
|
|
538
|
+
throw new Error("tmux target is not configured for this session");
|
|
539
|
+
}
|
|
540
|
+
const content = await (0, tmux_1.captureVisibleTmuxPane)(runtime.config.tmux, session.tmuxTarget, runtime.config.tmux.captureLines, runtime.config.webapp.visibleScreens);
|
|
541
|
+
return {
|
|
542
|
+
type: "live_response",
|
|
543
|
+
request_id: request.request_id,
|
|
544
|
+
ok: true,
|
|
545
|
+
result: {
|
|
546
|
+
session_id: session.sessionId,
|
|
547
|
+
session_label: session.label ?? null,
|
|
548
|
+
captured_at: new Date().toISOString(),
|
|
549
|
+
content,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (request.request_type === "action") {
|
|
554
|
+
const action = typeof request.payload?.action === "string"
|
|
555
|
+
? request.payload.action
|
|
556
|
+
: "";
|
|
557
|
+
if (!["up", "down", "enter", "slash", "delete", "tab", "escape"].includes(action)) {
|
|
558
|
+
throw new Error("Unsupported action");
|
|
559
|
+
}
|
|
560
|
+
const sessionId = request.local_session_id.trim();
|
|
561
|
+
const session = await runtime.sessionStore.getSession(sessionId);
|
|
562
|
+
if (!session?.tmuxTarget) {
|
|
563
|
+
throw new Error("tmux target is not configured for this session");
|
|
564
|
+
}
|
|
565
|
+
await (0, tmux_1.sendAllowedTmuxAction)(runtime.config.tmux, session.tmuxTarget, action);
|
|
566
|
+
return {
|
|
567
|
+
type: "live_response",
|
|
568
|
+
request_id: request.request_id,
|
|
569
|
+
ok: true,
|
|
570
|
+
result: {
|
|
571
|
+
ok: true,
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
throw new Error(`Unsupported live request type '${request.request_type}'`);
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
return {
|
|
579
|
+
type: "live_response",
|
|
580
|
+
request_id: request.request_id,
|
|
581
|
+
ok: false,
|
|
582
|
+
error: formatTmuxRelayError(error),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
async handleGatewayWsServerMessage(socket, raw) {
|
|
587
|
+
const runtime = this.getRuntimeOrThrow();
|
|
588
|
+
const parsed = JSON.parse(String(raw));
|
|
589
|
+
if (parsed.type === "hello") {
|
|
590
|
+
const hello = {
|
|
591
|
+
type: "hello",
|
|
592
|
+
connection_id: typeof parsed.connection_id === "string" && parsed.connection_id.trim()
|
|
593
|
+
? parsed.connection_id.trim()
|
|
594
|
+
: (0, node_crypto_1.randomUUID)(),
|
|
595
|
+
role: parsed.role === "gateway" ? "gateway" : "client",
|
|
596
|
+
...(typeof parsed.client_uuid === "string" && parsed.client_uuid.trim()
|
|
597
|
+
? { client_uuid: parsed.client_uuid.trim() }
|
|
598
|
+
: {}),
|
|
599
|
+
...(typeof parsed.project_name === "string" && parsed.project_name.trim()
|
|
600
|
+
? { project_name: parsed.project_name.trim() }
|
|
601
|
+
: {}),
|
|
602
|
+
...(typeof parsed.node_id === "string" && parsed.node_id.trim()
|
|
603
|
+
? { node_id: parsed.node_id.trim() }
|
|
604
|
+
: {}),
|
|
605
|
+
...(typeof parsed.package_version === "string" &&
|
|
606
|
+
parsed.package_version.trim()
|
|
607
|
+
? { package_version: parsed.package_version.trim() }
|
|
608
|
+
: {}),
|
|
609
|
+
...(typeof parsed.protocol_version === "string" &&
|
|
610
|
+
parsed.protocol_version.trim()
|
|
611
|
+
? { protocol_version: parsed.protocol_version.trim() }
|
|
612
|
+
: {}),
|
|
613
|
+
...(Array.isArray(parsed.capabilities)
|
|
614
|
+
? {
|
|
615
|
+
capabilities: parsed.capabilities
|
|
616
|
+
.map((item) => typeof item === "string" && item.trim()
|
|
617
|
+
? item.trim()
|
|
618
|
+
: null)
|
|
619
|
+
.filter((item) => Boolean(item)),
|
|
620
|
+
}
|
|
621
|
+
: {}),
|
|
622
|
+
...(Array.isArray(parsed.session_tools)
|
|
623
|
+
? {
|
|
624
|
+
session_tools: parsed.session_tools
|
|
625
|
+
.map((item) => item && typeof item === "object"
|
|
626
|
+
? {
|
|
627
|
+
local_session_id: typeof item.local_session_id ===
|
|
628
|
+
"string" &&
|
|
629
|
+
item.local_session_id.trim()
|
|
630
|
+
? item.local_session_id.trim()
|
|
631
|
+
: null,
|
|
632
|
+
...(typeof item.session_label ===
|
|
633
|
+
"string" &&
|
|
634
|
+
item.session_label.trim()
|
|
635
|
+
? {
|
|
636
|
+
session_label: item.session_label.trim(),
|
|
637
|
+
}
|
|
638
|
+
: {}),
|
|
639
|
+
...(typeof item.tools_hash ===
|
|
640
|
+
"string" &&
|
|
641
|
+
item.tools_hash.trim()
|
|
642
|
+
? {
|
|
643
|
+
tools_hash: item.tools_hash.trim(),
|
|
644
|
+
}
|
|
645
|
+
: {}),
|
|
646
|
+
}
|
|
647
|
+
: null)
|
|
648
|
+
.filter((item) => Boolean(item?.local_session_id)),
|
|
649
|
+
}
|
|
650
|
+
: {}),
|
|
651
|
+
};
|
|
652
|
+
const previous = this.connectedClients?.get(socket);
|
|
653
|
+
if (previous?.client_uuid) {
|
|
654
|
+
this.connectedClientsByUuid?.delete(previous.client_uuid);
|
|
655
|
+
}
|
|
656
|
+
this.connectedClients?.set(socket, hello);
|
|
657
|
+
if (hello.client_uuid) {
|
|
658
|
+
this.connectedClientsByUuid?.set(hello.client_uuid, socket);
|
|
659
|
+
}
|
|
660
|
+
runtime.logger.info("Gateway WS hello received", hello);
|
|
661
|
+
const localVersionInfo = this.getLocalVersionInfo?.() ?? {
|
|
662
|
+
packageVersion: "0.0.0-unknown",
|
|
663
|
+
protocolVersion: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION,
|
|
664
|
+
capabilities: [...versionHandshake_1.TELLYMCP_CAPABILITIES],
|
|
665
|
+
};
|
|
666
|
+
const compatibility = (0, versionHandshake_1.evaluateVersionCompatibility)({
|
|
667
|
+
gatewayPackageVersion: localVersionInfo.packageVersion,
|
|
668
|
+
gatewayProtocolVersion: localVersionInfo.protocolVersion,
|
|
669
|
+
...(hello.package_version
|
|
670
|
+
? { clientPackageVersion: hello.package_version }
|
|
671
|
+
: {}),
|
|
672
|
+
...(hello.protocol_version
|
|
673
|
+
? { clientProtocolVersion: hello.protocol_version }
|
|
674
|
+
: {}),
|
|
675
|
+
});
|
|
676
|
+
const ackInstruction = compatibility.compatibility === "reject"
|
|
677
|
+
? "Upgrade this client before continuing. Gateway transport is blocked until protocol major versions match."
|
|
678
|
+
: compatibility.compatibility === "warn"
|
|
679
|
+
? "Client and gateway versions differ. Upgrade the older side and verify TOOLS.md before continuing sensitive work."
|
|
680
|
+
: "Version handshake passed.";
|
|
681
|
+
socket.send(JSON.stringify({
|
|
682
|
+
type: "hello_ack",
|
|
683
|
+
connection_id: hello.connection_id,
|
|
684
|
+
package_version: localVersionInfo.packageVersion,
|
|
685
|
+
protocol_version: localVersionInfo.protocolVersion,
|
|
686
|
+
capabilities: localVersionInfo.capabilities,
|
|
687
|
+
compatibility: compatibility.compatibility,
|
|
688
|
+
reasons: compatibility.reasons,
|
|
689
|
+
instruction: ackInstruction,
|
|
690
|
+
}));
|
|
691
|
+
if (compatibility.compatibility === "reject") {
|
|
692
|
+
runtime.logger.warn("Rejecting gateway WS client due to protocol incompatibility", {
|
|
693
|
+
clientUuid: hello.client_uuid,
|
|
694
|
+
clientPackageVersion: hello.package_version ?? null,
|
|
695
|
+
clientProtocolVersion: hello.protocol_version ?? null,
|
|
696
|
+
gatewayPackageVersion: localVersionInfo.packageVersion,
|
|
697
|
+
gatewayProtocolVersion: localVersionInfo.protocolVersion,
|
|
698
|
+
reasons: compatibility.reasons,
|
|
699
|
+
});
|
|
700
|
+
setTimeout(() => {
|
|
701
|
+
try {
|
|
702
|
+
socket.close?.(4002, "version_incompatible");
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
socket.terminate?.();
|
|
706
|
+
}
|
|
707
|
+
}, 50);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
await this.notifyToolsMismatchForSocket?.(socket, hello);
|
|
711
|
+
if (hello.client_uuid) {
|
|
712
|
+
const queued = await this.broker.call("telegramMcp.gateway.pollDeliveries", {
|
|
713
|
+
client_uuid: hello.client_uuid,
|
|
714
|
+
limit: 50,
|
|
715
|
+
}, { meta: { internal_call: true } });
|
|
716
|
+
for (const delivery of Array.isArray(queued.deliveries)
|
|
717
|
+
? queued.deliveries
|
|
718
|
+
: []) {
|
|
719
|
+
socket.send(JSON.stringify({
|
|
720
|
+
type: "delivery_event",
|
|
721
|
+
event: "incoming_delivery",
|
|
722
|
+
payload: delivery,
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
const statuses = await this.broker.call("telegramMcp.gateway.listSenderDeliveryStatuses", {
|
|
726
|
+
client_uuid: hello.client_uuid,
|
|
727
|
+
limit: 100,
|
|
728
|
+
}, { meta: { internal_call: true } });
|
|
729
|
+
for (const status of Array.isArray(statuses.deliveries)
|
|
730
|
+
? statuses.deliveries
|
|
731
|
+
: []) {
|
|
732
|
+
socket.send(JSON.stringify({
|
|
733
|
+
type: "delivery_status_event",
|
|
734
|
+
payload: status,
|
|
735
|
+
}));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (parsed.type === "heartbeat_ping") {
|
|
741
|
+
socket.send(JSON.stringify({
|
|
742
|
+
type: "heartbeat_pong",
|
|
743
|
+
ts: typeof parsed.ts === "string" && parsed.ts.trim()
|
|
744
|
+
? parsed.ts.trim()
|
|
745
|
+
: new Date().toISOString(),
|
|
746
|
+
}));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (parsed.type === "live_response") {
|
|
750
|
+
const requestId = typeof parsed.request_id === "string" ? parsed.request_id.trim() : "";
|
|
751
|
+
if (!requestId) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const pending = this.pendingLiveRequests?.get(requestId);
|
|
755
|
+
if (!pending) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
clearTimeout(pending.timeout);
|
|
759
|
+
this.pendingLiveRequests?.delete(requestId);
|
|
760
|
+
if (parsed.ok === true) {
|
|
761
|
+
pending.resolve(parsed.result);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
pending.reject(new Error(typeof parsed.error === "string" && parsed.error.trim()
|
|
765
|
+
? parsed.error
|
|
766
|
+
: "Live relay request failed"));
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (parsed.type === "delivery_ack" || parsed.type === "delivery_fail") {
|
|
771
|
+
const hello = this.connectedClients?.get(socket);
|
|
772
|
+
const clientUuid = hello?.client_uuid?.trim();
|
|
773
|
+
if (!clientUuid) {
|
|
774
|
+
throw new Error("Gateway WS delivery update requires hello client_uuid");
|
|
775
|
+
}
|
|
776
|
+
const deliveryIds = Array.isArray(parsed.delivery_ids)
|
|
777
|
+
? parsed.delivery_ids
|
|
778
|
+
.map((item) => typeof item === "string" && item.trim() ? item.trim() : null)
|
|
779
|
+
.filter((item) => Boolean(item))
|
|
780
|
+
: [];
|
|
781
|
+
if (deliveryIds.length === 0) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const result = await this.broker.call(parsed.type === "delivery_ack"
|
|
785
|
+
? "telegramMcp.gateway.ackDeliveries"
|
|
786
|
+
: "telegramMcp.gateway.failDeliveries", {
|
|
787
|
+
client_uuid: clientUuid,
|
|
788
|
+
delivery_ids: deliveryIds,
|
|
789
|
+
...(parsed.type === "delivery_fail" &&
|
|
790
|
+
typeof parsed.error_text === "string" &&
|
|
791
|
+
parsed.error_text.trim()
|
|
792
|
+
? { error_text: parsed.error_text.trim() }
|
|
793
|
+
: {}),
|
|
794
|
+
}, { meta: { internal_call: true } });
|
|
795
|
+
for (const status of Array.isArray(result.deliveries)
|
|
796
|
+
? result.deliveries
|
|
797
|
+
: []) {
|
|
798
|
+
if (!status.source_client_uuid) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const publishResult = await this.broker.call("telegramMcp.gatewayRmq.publishDeliveryStatus", {
|
|
802
|
+
clientUuid: status.source_client_uuid,
|
|
803
|
+
status,
|
|
804
|
+
}, { meta: { internal_call: true } });
|
|
805
|
+
if (!publishResult?.published) {
|
|
806
|
+
await this.notifyDeliveryStatus?.({
|
|
807
|
+
clientUuid: status.source_client_uuid,
|
|
808
|
+
status,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
async handleGatewayWsClientMessage(raw) {
|
|
815
|
+
const runtime = this.getRuntimeOrThrow();
|
|
816
|
+
const parsed = JSON.parse(String(raw));
|
|
817
|
+
if (parsed.type === "hello_ack") {
|
|
818
|
+
const compatibility = parsed.compatibility === "warn" || parsed.compatibility === "reject"
|
|
819
|
+
? parsed.compatibility
|
|
820
|
+
: "ok";
|
|
821
|
+
const gatewayPackageVersion = typeof parsed.package_version === "string" && parsed.package_version.trim()
|
|
822
|
+
? parsed.package_version.trim()
|
|
823
|
+
: "0.0.0-unknown";
|
|
824
|
+
const gatewayProtocolVersion = typeof parsed.protocol_version === "string" && parsed.protocol_version.trim()
|
|
825
|
+
? parsed.protocol_version.trim()
|
|
826
|
+
: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION;
|
|
827
|
+
const reasons = Array.isArray(parsed.reasons)
|
|
828
|
+
? parsed.reasons
|
|
829
|
+
.map((item) => typeof item === "string" && item.trim() ? item.trim() : null)
|
|
830
|
+
.filter((item) => Boolean(item))
|
|
831
|
+
: [];
|
|
832
|
+
const gatewayCapabilities = Array.isArray(parsed.capabilities)
|
|
833
|
+
? parsed.capabilities
|
|
834
|
+
.map((item) => typeof item === "string" && item.trim() ? item.trim() : null)
|
|
835
|
+
.filter((item) => Boolean(item))
|
|
836
|
+
: [];
|
|
837
|
+
const localVersionInfo = this.getLocalVersionInfo?.() ?? {
|
|
838
|
+
packageVersion: "0.0.0-unknown",
|
|
839
|
+
protocolVersion: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION,
|
|
840
|
+
capabilities: [...versionHandshake_1.TELLYMCP_CAPABILITIES],
|
|
841
|
+
};
|
|
842
|
+
runtime.logger.info("Gateway WS hello acknowledged", {
|
|
843
|
+
connectionId: typeof parsed.connection_id === "string" ? parsed.connection_id : null,
|
|
844
|
+
clientUuid: this.wsHelloClientUuid,
|
|
845
|
+
compatibility,
|
|
846
|
+
gatewayPackageVersion,
|
|
847
|
+
gatewayProtocolVersion,
|
|
848
|
+
});
|
|
849
|
+
if (compatibility !== "ok") {
|
|
850
|
+
const sessionTools = Array.isArray(this.wsHelloSessionTools)
|
|
851
|
+
? this.wsHelloSessionTools
|
|
852
|
+
: [];
|
|
853
|
+
for (const sessionTool of sessionTools) {
|
|
854
|
+
await runtime.telegramTransport.handleGatewayVersionCompatibilityEvent({
|
|
855
|
+
local_session_id: sessionTool.local_session_id,
|
|
856
|
+
...(sessionTool.session_label
|
|
857
|
+
? { session_label: sessionTool.session_label }
|
|
858
|
+
: {}),
|
|
859
|
+
compatibility,
|
|
860
|
+
gateway_package_version: gatewayPackageVersion,
|
|
861
|
+
gateway_protocol_version: gatewayProtocolVersion,
|
|
862
|
+
gateway_capabilities: gatewayCapabilities,
|
|
863
|
+
client_package_version: localVersionInfo.packageVersion,
|
|
864
|
+
client_protocol_version: localVersionInfo.protocolVersion,
|
|
865
|
+
client_capabilities: localVersionInfo.capabilities,
|
|
866
|
+
reasons,
|
|
867
|
+
instruction: typeof parsed.instruction === "string" && parsed.instruction.trim()
|
|
868
|
+
? parsed.instruction.trim()
|
|
869
|
+
: compatibility === "reject"
|
|
870
|
+
? "Upgrade this client before continuing. Gateway transport is blocked until protocol major versions match."
|
|
871
|
+
: "Client and gateway versions differ. Upgrade the older side and verify TOOLS.md before continuing sensitive work.",
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (compatibility === "reject") {
|
|
876
|
+
try {
|
|
877
|
+
this.wsClient?.close?.(4002, "version_incompatible");
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
this.wsClient?.terminate?.();
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
await this.syncLocalToolsAgainstGateway?.();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (parsed.type === "heartbeat_pong") {
|
|
888
|
+
this.wsAwaitingPong = false;
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (parsed.type === "tools_event" && parsed.payload) {
|
|
892
|
+
await runtime.telegramTransport.handleToolsUpdatedEvent(parsed.payload);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (parsed.type === "project_event") {
|
|
896
|
+
if (parsed.event === "member_joined" &&
|
|
897
|
+
parsed.payload &&
|
|
898
|
+
typeof parsed.payload === "object") {
|
|
899
|
+
await runtime.telegramTransport.handleProjectMemberJoinedEvent(parsed.payload);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (parsed.event === "member_left" &&
|
|
903
|
+
parsed.payload &&
|
|
904
|
+
typeof parsed.payload === "object") {
|
|
905
|
+
await runtime.telegramTransport.handleProjectMemberLeftEvent(parsed.payload);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (parsed.event === "project_deleted" &&
|
|
909
|
+
parsed.payload &&
|
|
910
|
+
typeof parsed.payload === "object") {
|
|
911
|
+
await runtime.telegramTransport.handleProjectDeletedEvent(parsed.payload);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (parsed.type === "live_event" && parsed.payload) {
|
|
916
|
+
if (parsed.event === "approval_request") {
|
|
917
|
+
await runtime.telegramTransport.handleLiveViewApprovalRequestEvent(parsed.payload);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (parsed.event === "approval_granted" ||
|
|
921
|
+
parsed.event === "approval_denied") {
|
|
922
|
+
await runtime.telegramTransport.handleLiveViewApprovalResolvedEvent({
|
|
923
|
+
approved: parsed.event === "approval_granted",
|
|
924
|
+
...parsed.payload,
|
|
925
|
+
});
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (parsed.type === "delivery_event") {
|
|
930
|
+
const delivery = parsed.payload;
|
|
931
|
+
if (!delivery) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
await this.broker.call("telegramMcp.gatewayDelivery.materializeIncomingDelivery", {
|
|
936
|
+
delivery,
|
|
937
|
+
}, { meta: { internal_call: true } });
|
|
938
|
+
this.wsClient?.send(JSON.stringify({
|
|
939
|
+
type: "delivery_ack",
|
|
940
|
+
delivery_ids: [delivery.delivery_uuid],
|
|
941
|
+
}));
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
this.wsClient?.send(JSON.stringify({
|
|
945
|
+
type: "delivery_fail",
|
|
946
|
+
delivery_ids: [delivery.delivery_uuid],
|
|
947
|
+
error_text: error instanceof Error ? error.message : String(error),
|
|
948
|
+
}));
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (parsed.type === "delivery_status_event") {
|
|
954
|
+
await this.broker.call("telegramMcp.gatewayDelivery.applyOutgoingDeliveryStatus", {
|
|
955
|
+
status: parsed.payload,
|
|
956
|
+
}, { meta: { internal_call: true } });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (parsed.type === "live_request") {
|
|
960
|
+
const request = parsed;
|
|
961
|
+
const response = await this.processLiveRequest?.(request);
|
|
962
|
+
this.wsClient?.send(JSON.stringify(response));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
runtime.logger.debug("Gateway WS client message received", {
|
|
966
|
+
payload: String(raw),
|
|
967
|
+
});
|
|
968
|
+
},
|
|
969
|
+
async requestLiveRelay(params) {
|
|
970
|
+
const runtime = this.getRuntimeOrThrow();
|
|
971
|
+
const socket = this.connectedClientsByUuid?.get(params.clientUuid);
|
|
972
|
+
if (!socket || socket.readyState !== 1) {
|
|
973
|
+
throw new Error(`Gateway WS client '${params.clientUuid}' is not connected`);
|
|
974
|
+
}
|
|
975
|
+
const requestId = (0, node_crypto_1.randomUUID)();
|
|
976
|
+
const response = await new Promise((resolve, reject) => {
|
|
977
|
+
const timeout = setTimeout(() => {
|
|
978
|
+
this.pendingLiveRequests?.delete(requestId);
|
|
979
|
+
reject(new Error("Live relay WS request timed out"));
|
|
980
|
+
}, LIVE_REQUEST_TIMEOUT_MS);
|
|
981
|
+
this.pendingLiveRequests?.set(requestId, {
|
|
982
|
+
clientUuid: params.clientUuid,
|
|
983
|
+
resolve,
|
|
984
|
+
reject,
|
|
985
|
+
timeout,
|
|
986
|
+
});
|
|
987
|
+
const request = {
|
|
988
|
+
type: "live_request",
|
|
989
|
+
request_id: requestId,
|
|
990
|
+
request_type: params.requestType,
|
|
991
|
+
local_session_id: params.localSessionId,
|
|
992
|
+
payload: params.payload,
|
|
993
|
+
};
|
|
994
|
+
socket.send(JSON.stringify(request));
|
|
995
|
+
});
|
|
996
|
+
runtime.logger.debug("Gateway WS live relay completed", {
|
|
997
|
+
clientUuid: params.clientUuid,
|
|
998
|
+
localSessionId: params.localSessionId,
|
|
999
|
+
requestType: params.requestType,
|
|
1000
|
+
});
|
|
1001
|
+
return response;
|
|
1002
|
+
},
|
|
1003
|
+
async notifyProjectMemberJoined(params) {
|
|
1004
|
+
const message = {
|
|
1005
|
+
type: "project_event",
|
|
1006
|
+
event: "member_joined",
|
|
1007
|
+
payload: {
|
|
1008
|
+
project_uuid: params.projectUuid,
|
|
1009
|
+
project_name: params.projectName,
|
|
1010
|
+
...(params.memberDisplayName
|
|
1011
|
+
? { member_display_name: params.memberDisplayName }
|
|
1012
|
+
: {}),
|
|
1013
|
+
...(params.memberTelegramUsername
|
|
1014
|
+
? { member_telegram_username: params.memberTelegramUsername }
|
|
1015
|
+
: {}),
|
|
1016
|
+
},
|
|
1017
|
+
};
|
|
1018
|
+
let delivered = 0;
|
|
1019
|
+
for (const clientUuid of params.clientUuids) {
|
|
1020
|
+
if (await this.isLocalGatewayClientUuid?.(clientUuid)) {
|
|
1021
|
+
const runtime = this.getRuntimeOrThrow();
|
|
1022
|
+
await runtime.telegramTransport.handleProjectMemberJoinedEvent(message.payload);
|
|
1023
|
+
delivered += 1;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const socket = this.connectedClientsByUuid?.get(clientUuid);
|
|
1027
|
+
if (!socket || socket.readyState !== 1) {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
socket.send(JSON.stringify(message));
|
|
1031
|
+
delivered += 1;
|
|
1032
|
+
}
|
|
1033
|
+
return delivered;
|
|
1034
|
+
},
|
|
1035
|
+
async notifyProjectMemberLeft(params) {
|
|
1036
|
+
const message = {
|
|
1037
|
+
type: "project_event",
|
|
1038
|
+
event: "member_left",
|
|
1039
|
+
payload: {
|
|
1040
|
+
project_uuid: params.projectUuid,
|
|
1041
|
+
project_name: params.projectName,
|
|
1042
|
+
...(params.memberDisplayName
|
|
1043
|
+
? { member_display_name: params.memberDisplayName }
|
|
1044
|
+
: {}),
|
|
1045
|
+
...(params.memberTelegramUsername
|
|
1046
|
+
? { member_telegram_username: params.memberTelegramUsername }
|
|
1047
|
+
: {}),
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
let delivered = 0;
|
|
1051
|
+
for (const clientUuid of params.clientUuids) {
|
|
1052
|
+
if (await this.isLocalGatewayClientUuid?.(clientUuid)) {
|
|
1053
|
+
const runtime = this.getRuntimeOrThrow();
|
|
1054
|
+
await runtime.telegramTransport.handleProjectMemberLeftEvent(message.payload);
|
|
1055
|
+
delivered += 1;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const socket = this.connectedClientsByUuid?.get(clientUuid);
|
|
1059
|
+
if (!socket || socket.readyState !== 1) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
socket.send(JSON.stringify(message));
|
|
1063
|
+
delivered += 1;
|
|
1064
|
+
}
|
|
1065
|
+
return delivered;
|
|
1066
|
+
},
|
|
1067
|
+
async notifyProjectDeleted(params) {
|
|
1068
|
+
const message = {
|
|
1069
|
+
type: "project_event",
|
|
1070
|
+
event: "project_deleted",
|
|
1071
|
+
payload: {
|
|
1072
|
+
project_uuid: params.projectUuid,
|
|
1073
|
+
project_name: params.projectName,
|
|
1074
|
+
},
|
|
1075
|
+
};
|
|
1076
|
+
let delivered = 0;
|
|
1077
|
+
for (const clientUuid of params.clientUuids) {
|
|
1078
|
+
if (await this.isLocalGatewayClientUuid?.(clientUuid)) {
|
|
1079
|
+
const runtime = this.getRuntimeOrThrow();
|
|
1080
|
+
await runtime.telegramTransport.handleProjectDeletedEvent(message.payload);
|
|
1081
|
+
delivered += 1;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const socket = this.connectedClientsByUuid?.get(clientUuid);
|
|
1085
|
+
if (!socket || socket.readyState !== 1) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
socket.send(JSON.stringify(message));
|
|
1089
|
+
delivered += 1;
|
|
1090
|
+
}
|
|
1091
|
+
return delivered;
|
|
1092
|
+
},
|
|
1093
|
+
async notifyLiveApprovalRequest(params) {
|
|
1094
|
+
if (await this.isLocalGatewayClientUuid?.(params.clientUuid)) {
|
|
1095
|
+
const runtime = this.getRuntimeOrThrow();
|
|
1096
|
+
await runtime.telegramTransport.handleLiveViewApprovalRequestEvent(params.payload);
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
const socket = this.connectedClientsByUuid?.get(params.clientUuid);
|
|
1100
|
+
if (!socket || socket.readyState !== 1) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
socket.send(JSON.stringify({
|
|
1104
|
+
type: "live_event",
|
|
1105
|
+
event: "approval_request",
|
|
1106
|
+
payload: params.payload,
|
|
1107
|
+
}));
|
|
1108
|
+
return true;
|
|
1109
|
+
},
|
|
1110
|
+
async notifyLiveApprovalResolved(params) {
|
|
1111
|
+
if (await this.isLocalGatewayClientUuid?.(params.clientUuid)) {
|
|
1112
|
+
const runtime = this.getRuntimeOrThrow();
|
|
1113
|
+
await runtime.telegramTransport.handleLiveViewApprovalResolvedEvent({
|
|
1114
|
+
approved: params.approved,
|
|
1115
|
+
...params.payload,
|
|
1116
|
+
});
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
const socket = this.connectedClientsByUuid?.get(params.clientUuid);
|
|
1120
|
+
if (!socket || socket.readyState !== 1) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
socket.send(JSON.stringify({
|
|
1124
|
+
type: "live_event",
|
|
1125
|
+
event: params.approved ? "approval_granted" : "approval_denied",
|
|
1126
|
+
payload: params.payload,
|
|
1127
|
+
}));
|
|
1128
|
+
return true;
|
|
1129
|
+
},
|
|
1130
|
+
async notifyDeliveryQueued(params) {
|
|
1131
|
+
if (await this.handleLocalIncomingDelivery?.(params)) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
const socket = this.connectedClientsByUuid?.get(params.clientUuid);
|
|
1135
|
+
if (!socket || socket.readyState !== 1) {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
socket.send(JSON.stringify({
|
|
1139
|
+
type: "delivery_event",
|
|
1140
|
+
event: "incoming_delivery",
|
|
1141
|
+
payload: params.delivery,
|
|
1142
|
+
}));
|
|
1143
|
+
return true;
|
|
1144
|
+
},
|
|
1145
|
+
async notifyDeliveryStatus(params) {
|
|
1146
|
+
if (await this.handleLocalDeliveryStatus?.(params)) {
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
const socket = this.connectedClientsByUuid?.get(params.clientUuid);
|
|
1150
|
+
if (!socket || socket.readyState !== 1) {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
socket.send(JSON.stringify({
|
|
1154
|
+
type: "delivery_status_event",
|
|
1155
|
+
payload: params.status,
|
|
1156
|
+
}));
|
|
1157
|
+
return true;
|
|
1158
|
+
},
|
|
1159
|
+
async startGatewayWsServer() {
|
|
1160
|
+
const runtime = this.getRuntimeOrThrow();
|
|
1161
|
+
if (!runtime || this.wsServer) {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const httpServer = this.getHttpServerOrThrow?.();
|
|
1165
|
+
const wsPath = runtime.config.distributed.gatewayWsPath.replace(/\/+$/u, "") || "/";
|
|
1166
|
+
const wsServer = new WebSocketServer({ noServer: true });
|
|
1167
|
+
wsServer.on("connection", (socket, req) => {
|
|
1168
|
+
if (runtime.config.distributed.gatewayAuthToken) {
|
|
1169
|
+
const authorization = req.headers?.authorization;
|
|
1170
|
+
if (authorization !==
|
|
1171
|
+
`Bearer ${runtime.config.distributed.gatewayAuthToken}`) {
|
|
1172
|
+
socket.close(1008, "Unauthorized");
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
runtime.logger.warn("Gateway WS client connected", {
|
|
1177
|
+
remoteAddress: req.socket.remoteAddress,
|
|
1178
|
+
path: req.url,
|
|
1179
|
+
});
|
|
1180
|
+
socket.on("message", (raw) => {
|
|
1181
|
+
void this.handleGatewayWsServerMessage?.(socket, raw).catch((error) => {
|
|
1182
|
+
runtime.logger.warn("Gateway WS server message parse failed", {
|
|
1183
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
socket.on("close", () => {
|
|
1188
|
+
const hello = this.connectedClients?.get(socket);
|
|
1189
|
+
if (hello?.client_uuid) {
|
|
1190
|
+
this.connectedClientsByUuid?.delete(hello.client_uuid);
|
|
1191
|
+
}
|
|
1192
|
+
this.connectedClientToolsAlerts?.delete(socket);
|
|
1193
|
+
this.connectedClients?.delete(socket);
|
|
1194
|
+
runtime.logger.warn("Gateway WS client disconnected", {
|
|
1195
|
+
connectionId: hello?.connection_id ?? null,
|
|
1196
|
+
clientUuid: hello?.client_uuid ?? null,
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
const upgradeHandler = (req, socket, head) => {
|
|
1201
|
+
const requestUrl = new URL(req.url ?? "/", "http://gateway.local");
|
|
1202
|
+
const requestPath = requestUrl.pathname.replace(/\/+$/u, "") || "/";
|
|
1203
|
+
if (requestPath !== wsPath) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
wsServer.handleUpgrade(req, socket, head, (clientSocket) => {
|
|
1207
|
+
wsServer.emit("connection", clientSocket, req);
|
|
1208
|
+
});
|
|
1209
|
+
};
|
|
1210
|
+
httpServer?.on("upgrade", upgradeHandler);
|
|
1211
|
+
this.wsServer = wsServer;
|
|
1212
|
+
this.wsUpgradeHandler = upgradeHandler;
|
|
1213
|
+
runtime.logger.warn("Gateway WS server attached", {
|
|
1214
|
+
path: runtime.config.distributed.gatewayWsPath,
|
|
1215
|
+
});
|
|
1216
|
+
},
|
|
1217
|
+
ensureGatewayWsClientIsReusable() {
|
|
1218
|
+
if (!this.wsClient) {
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
const readyState = typeof this.wsClient.readyState === "number"
|
|
1222
|
+
? this.wsClient.readyState
|
|
1223
|
+
: null;
|
|
1224
|
+
// OPEN=1, CONNECTING=0 are still active; CLOSING=2 and CLOSED=3 are stale.
|
|
1225
|
+
if (readyState === 0 || readyState === 1) {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
this.wsClient = null;
|
|
1229
|
+
return true;
|
|
1230
|
+
},
|
|
1231
|
+
scheduleGatewayWsReconnect() {
|
|
1232
|
+
if (this.stopRequested || this.wsReconnectTimer) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const runtime = this.getRuntimeOrThrow?.();
|
|
1236
|
+
runtime?.logger.warn("Gateway WS reconnect scheduled", {
|
|
1237
|
+
delayMs: CLIENT_RECONNECT_DELAY_MS,
|
|
1238
|
+
});
|
|
1239
|
+
this.wsReconnectTimer = setTimeout(() => {
|
|
1240
|
+
this.wsReconnectTimer = null;
|
|
1241
|
+
void this.startGatewayWsClient?.();
|
|
1242
|
+
}, CLIENT_RECONNECT_DELAY_MS);
|
|
1243
|
+
},
|
|
1244
|
+
async startGatewayWsClient() {
|
|
1245
|
+
const runtime = this.getRuntimeOrThrow?.();
|
|
1246
|
+
if (!runtime) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (!this.ensureGatewayWsClientIsReusable?.()) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const wsUrl = runtime.config.distributed.gatewayWsUrl;
|
|
1253
|
+
if (!wsUrl) {
|
|
1254
|
+
runtime.logger.info("Gateway WS client is disabled", {
|
|
1255
|
+
reason: "GATEWAY_WS_URL is not configured",
|
|
1256
|
+
});
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const normalizedUrl = normalizeWebSocketUrl(wsUrl, runtime.config.distributed.gatewayWsPath);
|
|
1260
|
+
this.wsConnectionId = (0, node_crypto_1.randomUUID)();
|
|
1261
|
+
const socket = new wsLib.WebSocket(normalizedUrl, {
|
|
1262
|
+
headers: runtime.config.distributed.gatewayAuthToken
|
|
1263
|
+
? {
|
|
1264
|
+
authorization: `Bearer ${runtime.config.distributed.gatewayAuthToken}`,
|
|
1265
|
+
}
|
|
1266
|
+
: undefined,
|
|
1267
|
+
});
|
|
1268
|
+
socket.on("open", () => {
|
|
1269
|
+
runtime.logger.warn(this.wsClientHasConnectedOnce
|
|
1270
|
+
? "Gateway WS connected to gateway again"
|
|
1271
|
+
: "Gateway WS connected to gateway", {
|
|
1272
|
+
url: normalizedUrl,
|
|
1273
|
+
});
|
|
1274
|
+
this.wsClientHasConnectedOnce = true;
|
|
1275
|
+
this.wsAwaitingPong = false;
|
|
1276
|
+
void this.sendClientHello?.(socket);
|
|
1277
|
+
});
|
|
1278
|
+
socket.on("message", (raw) => {
|
|
1279
|
+
void this.handleGatewayWsClientMessage?.(raw).catch((error) => {
|
|
1280
|
+
runtime.logger.warn("Gateway WS client message handling failed", {
|
|
1281
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
socket.on("error", (error) => {
|
|
1286
|
+
runtime.logger.warn("Gateway WS client error", {
|
|
1287
|
+
url: normalizedUrl,
|
|
1288
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
1289
|
+
});
|
|
1290
|
+
this.wsAwaitingPong = false;
|
|
1291
|
+
if (this.wsClient === socket && socket.readyState !== 1) {
|
|
1292
|
+
this.wsClient = null;
|
|
1293
|
+
}
|
|
1294
|
+
this.scheduleGatewayWsReconnect?.();
|
|
1295
|
+
});
|
|
1296
|
+
socket.on("close", () => {
|
|
1297
|
+
runtime.logger.warn("Gateway WS connection to gateway closed", {
|
|
1298
|
+
url: normalizedUrl,
|
|
1299
|
+
clientUuid: this.wsHelloClientUuid,
|
|
1300
|
+
});
|
|
1301
|
+
this.wsAwaitingPong = false;
|
|
1302
|
+
if (this.wsClient === socket) {
|
|
1303
|
+
this.wsClient = null;
|
|
1304
|
+
}
|
|
1305
|
+
this.scheduleGatewayWsReconnect?.();
|
|
1306
|
+
});
|
|
1307
|
+
this.wsClient = socket;
|
|
1308
|
+
if (!this.wsIdentityRefreshTimer) {
|
|
1309
|
+
this.wsIdentityRefreshTimer = setInterval(() => {
|
|
1310
|
+
if (this.stopRequested || !this.wsClient || this.wsClient.readyState !== 1) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
void (async () => {
|
|
1314
|
+
const currentClientUuid = await runtime.maintenanceStore.getGatewayClientUuid();
|
|
1315
|
+
const currentSessionTools = await this.collectSessionTools?.() ?? {
|
|
1316
|
+
sessionTools: [],
|
|
1317
|
+
snapshot: "[]",
|
|
1318
|
+
};
|
|
1319
|
+
if ((currentClientUuid ?? null) === this.wsHelloClientUuid &&
|
|
1320
|
+
currentSessionTools.snapshot === this.wsHelloSessionToolsSnapshot) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
await this.sendClientHello?.(this.wsClient);
|
|
1324
|
+
})().catch((error) => {
|
|
1325
|
+
runtime.logger.warn("Gateway WS hello refresh failed", {
|
|
1326
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
}, 5000);
|
|
1330
|
+
}
|
|
1331
|
+
if (!this.wsToolsSyncTimer) {
|
|
1332
|
+
this.wsToolsSyncTimer = setInterval(() => {
|
|
1333
|
+
if (this.stopRequested) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
void (async () => {
|
|
1337
|
+
for (const [clientSocket, hello] of this.connectedClients?.entries() ?? []) {
|
|
1338
|
+
if (!clientSocket || clientSocket.readyState !== 1) {
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
await this.notifyToolsMismatchForSocket?.(clientSocket, hello);
|
|
1342
|
+
}
|
|
1343
|
+
})().catch((error) => {
|
|
1344
|
+
runtime.logger.warn("Gateway WS tools sync check failed", {
|
|
1345
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
}, TOOLS_SYNC_CHECK_INTERVAL_MS);
|
|
1349
|
+
}
|
|
1350
|
+
if (!this.wsHeartbeatTimer) {
|
|
1351
|
+
this.wsHeartbeatTimer = setInterval(() => {
|
|
1352
|
+
if (this.stopRequested) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
const activeSocket = this.wsClient;
|
|
1356
|
+
if (!activeSocket || activeSocket.readyState !== 1) {
|
|
1357
|
+
this.wsAwaitingPong = false;
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (this.wsAwaitingPong) {
|
|
1361
|
+
runtime.logger.warn("Gateway WS heartbeat timed out; terminating stale client socket", {
|
|
1362
|
+
url: normalizedUrl,
|
|
1363
|
+
clientUuid: this.wsHelloClientUuid,
|
|
1364
|
+
});
|
|
1365
|
+
this.wsAwaitingPong = false;
|
|
1366
|
+
if (this.wsClient === activeSocket) {
|
|
1367
|
+
this.wsClient = null;
|
|
1368
|
+
}
|
|
1369
|
+
activeSocket.terminate?.();
|
|
1370
|
+
this.scheduleGatewayWsReconnect?.();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
this.wsAwaitingPong = true;
|
|
1374
|
+
try {
|
|
1375
|
+
activeSocket.send?.(JSON.stringify({
|
|
1376
|
+
type: "heartbeat_ping",
|
|
1377
|
+
ts: new Date().toISOString(),
|
|
1378
|
+
}));
|
|
1379
|
+
}
|
|
1380
|
+
catch (error) {
|
|
1381
|
+
this.wsAwaitingPong = false;
|
|
1382
|
+
runtime.logger.warn("Gateway WS heartbeat ping failed", {
|
|
1383
|
+
url: normalizedUrl,
|
|
1384
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
1385
|
+
});
|
|
1386
|
+
if (this.wsClient === activeSocket) {
|
|
1387
|
+
this.wsClient = null;
|
|
1388
|
+
}
|
|
1389
|
+
activeSocket.terminate?.();
|
|
1390
|
+
this.scheduleGatewayWsReconnect?.();
|
|
1391
|
+
}
|
|
1392
|
+
}, WS_HEARTBEAT_INTERVAL_MS);
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
async closeGatewayWsResources() {
|
|
1396
|
+
if (this.wsReconnectTimer) {
|
|
1397
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1398
|
+
this.wsReconnectTimer = null;
|
|
1399
|
+
}
|
|
1400
|
+
if (this.wsIdentityRefreshTimer) {
|
|
1401
|
+
clearInterval(this.wsIdentityRefreshTimer);
|
|
1402
|
+
this.wsIdentityRefreshTimer = null;
|
|
1403
|
+
}
|
|
1404
|
+
if (this.wsToolsSyncTimer) {
|
|
1405
|
+
clearInterval(this.wsToolsSyncTimer);
|
|
1406
|
+
this.wsToolsSyncTimer = null;
|
|
1407
|
+
}
|
|
1408
|
+
if (this.wsHeartbeatTimer) {
|
|
1409
|
+
clearInterval(this.wsHeartbeatTimer);
|
|
1410
|
+
this.wsHeartbeatTimer = null;
|
|
1411
|
+
}
|
|
1412
|
+
this.wsAwaitingPong = false;
|
|
1413
|
+
for (const pending of this.pendingLiveRequests?.values() ?? []) {
|
|
1414
|
+
clearTimeout(pending.timeout);
|
|
1415
|
+
pending.reject(new Error("Gateway WS transport is shutting down"));
|
|
1416
|
+
}
|
|
1417
|
+
this.pendingLiveRequests?.clear();
|
|
1418
|
+
if (this.wsClient) {
|
|
1419
|
+
const socket = this.wsClient;
|
|
1420
|
+
this.wsClient = null;
|
|
1421
|
+
socket.removeAllListeners();
|
|
1422
|
+
socket.close();
|
|
1423
|
+
}
|
|
1424
|
+
this.wsHelloSessionToolsSnapshot = null;
|
|
1425
|
+
if (this.wsServer) {
|
|
1426
|
+
const server = this.wsServer;
|
|
1427
|
+
this.wsServer = null;
|
|
1428
|
+
const httpServer = this.standaloneHttpService?.httpServer;
|
|
1429
|
+
if (httpServer && this.wsUpgradeHandler) {
|
|
1430
|
+
httpServer.removeListener("upgrade", this.wsUpgradeHandler);
|
|
1431
|
+
}
|
|
1432
|
+
this.wsUpgradeHandler = null;
|
|
1433
|
+
await new Promise((resolve, reject) => {
|
|
1434
|
+
server.close((error) => {
|
|
1435
|
+
if (error) {
|
|
1436
|
+
reject(error);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
resolve();
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
this.connectedClientToolsAlerts?.clear();
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
async started() {
|
|
1447
|
+
const runtime = this.getRuntimeOrThrow?.();
|
|
1448
|
+
const mode = runtime?.config.distributed.mode;
|
|
1449
|
+
const gatewayEnabled = mode === "gateway" || mode === "both";
|
|
1450
|
+
const clientEnabled = mode === "client" || mode === "both";
|
|
1451
|
+
if (gatewayEnabled) {
|
|
1452
|
+
await this.startGatewayWsServer?.();
|
|
1453
|
+
}
|
|
1454
|
+
if (clientEnabled) {
|
|
1455
|
+
await this.startGatewayWsClient?.();
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
async stopped() {
|
|
1459
|
+
this.stopRequested = true;
|
|
1460
|
+
await this.closeGatewayWsResources?.();
|
|
1461
|
+
},
|
|
1462
|
+
};
|
|
1463
|
+
exports.default = TelegramMcpGatewaySocketService;
|