@deadragdoll/tellymcp 0.0.13 → 0.0.15
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 +23 -39
- package/.env.example.gateway +52 -61
- package/CHANGELOG.md +57 -0
- package/README-ru.md +80 -6
- package/README.md +80 -6
- package/TOOLS.md +219 -10
- package/config/templates/env.both.template +19 -7
- package/config/templates/env.client.template +9 -19
- package/config/templates/env.gateway.template +19 -31
- package/dist/cli.js +252 -47
- package/dist/configureServer.js +966 -0
- package/dist/envMigration.js +316 -0
- package/dist/moleculer.config.js +1 -3
- package/dist/services/features/telegram-mcp/approval.service.js +1 -1
- package/dist/services/features/telegram-mcp/browser.service.js +18 -0
- package/dist/services/features/telegram-mcp/collaboration.service.js +2 -2
- package/dist/services/features/telegram-mcp/ensuredb.service.js +1 -1
- package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
- package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
- package/dist/services/features/telegram-mcp/gateway.service.js +1 -1
- package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +20 -0
- package/dist/services/features/telegram-mcp/session-context.service.js +25 -1
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +79 -67
- package/dist/services/features/telegram-mcp/src/app/config/env.js +129 -38
- package/dist/services/features/telegram-mcp/src/app/config/environmentContract.js +66 -0
- package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
- package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
- package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +88 -19
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +412 -29
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +37 -3
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +7 -6
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +214 -37
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +186 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/gatewaySessionsService.js +5 -5
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +5 -2
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayCollaborationBackend.js +4 -4
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -36
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/remoteConsoleActionClient.js +4 -3
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +9 -5
- package/dist/services/features/telegram-mcp/src/features/session-context/model/getRuntimeDiagnosticsTool.js +30 -0
- package/dist/services/features/telegram-mcp/src/features/session-context/model/sessionContextService.js +169 -7
- package/dist/services/features/telegram-mcp/src/shared/integrations/memory/processLocalStateStore.js +260 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/object-storage/minioExchangeStore.js +1 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +4 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +2 -2
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMessageFlow.js +1 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportProjectState.js +2 -2
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
- package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/gatewayScope.js +5 -5
- package/dist/services/features/telegram-mcp/src/shared/lib/project-identity/projectIdentity.js +10 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
- package/docs/CHAT_CONNECTOR.md +134 -0
- package/docs/STANDALONE-ru.md +41 -3
- package/docs/STANDALONE.md +41 -3
- package/package.json +5 -3
- package/packages/chrome-attach-extension/dist/background.js +572 -163
- package/packages/chrome-attach-extension/dist/manifest.json +2 -1
- package/packages/chrome-attach-extension/dist/options.js +15 -2
- package/packages/chrome-attach-extension/dist/recorder-content.js +14 -1
- package/packages/chrome-attach-extension/dist/recorder-page.js +34 -18
- package/packages/firefox-attach-extension/dist/background.js +413 -33
- package/packages/firefox-attach-extension/dist/manifest.json +0 -12
- package/packages/firefox-attach-extension/dist/options.js +14 -1
- package/packages/firefox-attach-extension/dist/recorder-content.js +14 -1
- package/packages/firefox-attach-extension/dist/recorder-page.js +34 -18
- package/scripts/postinstall.js +33 -1
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.deriveEnvironmentFromPublicBase = deriveEnvironmentFromPublicBase;
|
|
7
|
+
exports.renderConfiguredEnvironment = renderConfiguredEnvironment;
|
|
8
|
+
exports.validateConfiguredEnvironment = validateConfiguredEnvironment;
|
|
9
|
+
exports.runConfigureConnectionCheck = runConfigureConnectionCheck;
|
|
10
|
+
exports.startConfigureServer = startConfigureServer;
|
|
11
|
+
const node_http_1 = require("node:http");
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const dotenv_1 = require("dotenv");
|
|
15
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
16
|
+
const pg_1 = require("pg");
|
|
17
|
+
const ws_1 = __importDefault(require("ws"));
|
|
18
|
+
const amqplib_1 = require("amqplib");
|
|
19
|
+
const fieldLabels = {
|
|
20
|
+
PUBLIC_BASE_URL: "Публичный базовый URL",
|
|
21
|
+
OAUTH_ENABLED: "Включить OAuth-коннектор",
|
|
22
|
+
PROJECT_NAME: "Название проекта",
|
|
23
|
+
TELLYMCP_SESSION_ID: "Локальный ID сессии",
|
|
24
|
+
TELLYMCP_SESSION_LABEL: "Название консоли",
|
|
25
|
+
TELEGRAM_BOT_TOKEN: "Токен Telegram-бота",
|
|
26
|
+
TELEGRAM_BOT_USERNAME: "Username Telegram-бота",
|
|
27
|
+
ADMIN_TOKEN: "Администраторский токен",
|
|
28
|
+
DEBUG_LANGUAGE: "Язык интерфейса Telegram",
|
|
29
|
+
TELEGRAM_REQUEST_MODE: "Режим обработки запросов",
|
|
30
|
+
TELEGRAM_WEBHOOK_ENABLED: "Использовать Telegram webhook",
|
|
31
|
+
TELEGRAM_WEBHOOK_PATH: "Локальный путь webhook",
|
|
32
|
+
TELEGRAM_WEBHOOK_PUBLIC_URL: "Публичный URL webhook",
|
|
33
|
+
TELEGRAM_WEBHOOK_SECRET: "Секрет webhook",
|
|
34
|
+
TELEGRAM_WEBHOOK_TRACE: "Подробный trace webhook",
|
|
35
|
+
TELEGRAM_WEBHOOK_DROP_PENDING_UPDATES: "Сбросить старые Telegram updates",
|
|
36
|
+
REDIS_HOST: "Хост Redis",
|
|
37
|
+
REDIS_PORT: "Порт Redis",
|
|
38
|
+
REDIS_DB: "Номер базы Redis",
|
|
39
|
+
REDIS_USERNAME: "Пользователь Redis",
|
|
40
|
+
REDIS_PASSWORD: "Пароль Redis",
|
|
41
|
+
GATEWAY_PUBLIC_URL: "Публичный HTTP URL шлюза",
|
|
42
|
+
GATEWAY_WS_URL: "Публичный WebSocket URL шлюза",
|
|
43
|
+
GATEWAY_WS_PATH: "WebSocket-путь шлюза",
|
|
44
|
+
GATEWAY_AUTH_TOKEN: "Общий transport-токен шлюза",
|
|
45
|
+
GATEWAY_SCOPE_TOKEN: "Scope-токен регистрации",
|
|
46
|
+
GATEWAY_USER_UUID: "UUID владельца в шлюзе",
|
|
47
|
+
DB_HOST: "Хост PostgreSQL",
|
|
48
|
+
DB_PORT: "Порт PostgreSQL",
|
|
49
|
+
DB_USER: "Пользователь PostgreSQL",
|
|
50
|
+
DB_PASSWORD: "Пароль PostgreSQL",
|
|
51
|
+
DB_NAME: "База PostgreSQL",
|
|
52
|
+
DB_SCHEMA: "Схема PostgreSQL",
|
|
53
|
+
MCP_HTTP_HOST: "Адрес привязки MCP HTTP",
|
|
54
|
+
MCP_HTTP_PORT: "Порт MCP HTTP",
|
|
55
|
+
MCP_HTTP_PATH: "HTTP-путь MCP",
|
|
56
|
+
MCP_HTTP_BEARER_TOKEN: "Bearer-токен MCP",
|
|
57
|
+
MCP_HTTP_ENABLE_DEBUG_ROUTES: "Включить debug-маршруты MCP",
|
|
58
|
+
MCP_HTTP_ENABLE_PRUNE_ROUTE: "Включить системную очистку через HTTP",
|
|
59
|
+
MCP_XCHANGE_DIR: "Каталог обмена файлами",
|
|
60
|
+
ROOT_PREFIX: "Корневой URL-префикс шлюза",
|
|
61
|
+
PORT: "HTTP-порт шлюза",
|
|
62
|
+
TELLYMCP_PUBLIC_URL: "Публичный URL OAuth-коннектора",
|
|
63
|
+
TELLYMCP_OAUTH_ISSUER: "OAuth issuer",
|
|
64
|
+
TELLYMCP_OAUTH_AUDIENCE: "OAuth audience",
|
|
65
|
+
TELLYMCP_MAGIC_TOKEN: "Magic token коннектора",
|
|
66
|
+
TELLYMCP_MAGIC_TOKEN_HASH: "SHA-256 hash magic token",
|
|
67
|
+
TELLYMCP_OAUTH_CLIENT_ID: "OAuth client ID",
|
|
68
|
+
TELLYMCP_OAUTH_CLIENT_SECRET: "OAuth client secret",
|
|
69
|
+
TELLYMCP_ALLOWED_REDIRECT_URIS: "Разрешённые redirect URI",
|
|
70
|
+
TELLYMCP_OAUTH_PRIVATE_KEY_PEM: "Приватный ключ OAuth",
|
|
71
|
+
TELLYMCP_AUTH_CODE_TTL_SECONDS: "Срок жизни OAuth-кода, секунд",
|
|
72
|
+
TELLYMCP_OAUTH_SCOPES: "OAuth scopes",
|
|
73
|
+
TELLYMCP_OAUTH_KEY_ID: "ID ключа OAuth",
|
|
74
|
+
RMQ_HOST: "Хост RabbitMQ",
|
|
75
|
+
RMQ_PORT: "Порт RabbitMQ",
|
|
76
|
+
RMQ_USER: "Пользователь RabbitMQ",
|
|
77
|
+
RMQ_PASSWORD: "Пароль RabbitMQ",
|
|
78
|
+
RMQ_VHOST: "Virtual host RabbitMQ",
|
|
79
|
+
RMQ_EXCHANGE: "Exchange RabbitMQ",
|
|
80
|
+
WEBAPP_ENABLED: "Включить Telegram Mini App",
|
|
81
|
+
WEBAPP_BASE_PATH: "Локальный путь Mini App",
|
|
82
|
+
WEBAPP_PUBLIC_URL: "Публичный URL Mini App",
|
|
83
|
+
WEBAPP_INITDATA_TTL_SECONDS: "Срок жизни initData, секунд",
|
|
84
|
+
WEBAPP_SESSION_TTL_SECONDS: "Срок жизни Mini App сессии, секунд",
|
|
85
|
+
WEBAPP_LAUNCH_MODE: "Режим открытия Mini App",
|
|
86
|
+
WEBAPP_VISIBLE_SCREENS: "Количество видимых экранов",
|
|
87
|
+
WEBAPP_ACTION_COOLDOWN_MS: "Задержка между действиями, мс",
|
|
88
|
+
TERMINAL_SHELL: "Командная оболочка",
|
|
89
|
+
TERMINAL_COLS: "Ширина терминала",
|
|
90
|
+
TERMINAL_ROWS: "Высота терминала",
|
|
91
|
+
TERMINAL_SCROLLBACK_LINES: "Размер истории терминала",
|
|
92
|
+
TERMINAL_NUDGE_ENABLED: "Будить агента сообщениями",
|
|
93
|
+
TERMINAL_NUDGE_DEBOUNCE_SECONDS: "Debounce уведомлений, секунд",
|
|
94
|
+
TERMINAL_NUDGE_COOLDOWN_SECONDS: "Cooldown уведомлений, секунд",
|
|
95
|
+
TERMINAL_NUDGE_MESSAGE: "Инструкция для Telegram-задачи",
|
|
96
|
+
TERMINAL_PARTNER_NUDGE_MESSAGE: "Инструкция для partner-задачи",
|
|
97
|
+
TERMINAL_PARTNER_REPLY_NUDGE_MESSAGE: "Инструкция для partner-ответа",
|
|
98
|
+
TERMINAL_CAPTURE_MODE: "Режим захвата терминала",
|
|
99
|
+
TERMINAL_CAPTURE_LINES: "Количество строк захвата",
|
|
100
|
+
TERMINAL_PROMPT_SCAN_ENABLED: "Распознавать prompt терминала",
|
|
101
|
+
TERMINAL_PROMPT_SCAN_INTERVAL_SECONDS: "Интервал проверки prompt, секунд",
|
|
102
|
+
TERMINAL_PROMPT_SCAN_COOLDOWN_SECONDS: "Cooldown проверки prompt, секунд",
|
|
103
|
+
TERMINAL_PROMPT_SCAN_STRATEGY: "Стратегия распознавания prompt",
|
|
104
|
+
TERMINAL_PROMPT_SCAN_MIN_SCORE: "Минимальный score prompt",
|
|
105
|
+
BROWSER_ENABLED: "Включить browser tools",
|
|
106
|
+
BROWSER_HEADLESS: "Запускать браузер без окна",
|
|
107
|
+
BROWSER_DEVTOOLS: "Открывать DevTools",
|
|
108
|
+
BROWSER_ADDRESS: "Стартовый адрес браузера",
|
|
109
|
+
BROWSER_TIMEOUT_MS: "Timeout browser-действий, мс",
|
|
110
|
+
BROWSER_MAX_EVENTS: "Лимит browser-событий",
|
|
111
|
+
BROWSER_WAIT_UNTIL: "Условие готовности страницы",
|
|
112
|
+
BROWSER_EXECUTABLE_PATH: "Путь к браузеру",
|
|
113
|
+
BROWSER_CHANNEL: "Канал браузера",
|
|
114
|
+
BROWSER_SLOW_MO_MS: "Замедление browser-действий, мс",
|
|
115
|
+
BROWSER_ATTACH_ENABLED: "Разрешить browser extension attach",
|
|
116
|
+
BROWSER_ATTACH_WS_HOST: "Хост extension WebSocket",
|
|
117
|
+
BROWSER_ATTACH_WS_PORT: "Порт extension WebSocket",
|
|
118
|
+
BROWSER_ATTACH_WS_PATH: "Путь extension WebSocket",
|
|
119
|
+
NAMESPACE: "Namespace Moleculer",
|
|
120
|
+
NODE_ID: "ID узла Moleculer",
|
|
121
|
+
LOG_LEVEL: "Уровень логирования",
|
|
122
|
+
LOG_FILE_ENABLED: "Записывать лог в файл",
|
|
123
|
+
LOG_FILE_PATH: "Путь к файлу логов",
|
|
124
|
+
LOGFEED_ENABLED: "Включить поток логов",
|
|
125
|
+
};
|
|
126
|
+
const fieldHelp = {
|
|
127
|
+
PUBLIC_BASE_URL: "Укажите домен или API base один раз. HTTP, WebSocket, Mini App и webhook URL будут рассчитаны автоматически.",
|
|
128
|
+
OAUTH_ENABLED: "Если включить, public URL, issuer и audience будут рассчитаны из того же базового URL.",
|
|
129
|
+
GATEWAY_PUBLIC_URL: "HTTP endpoint, через который клиент обращается к API шлюза.",
|
|
130
|
+
GATEWAY_WS_URL: "WebSocket endpoint для live relay и управления консолью.",
|
|
131
|
+
GATEWAY_AUTH_TOKEN: "Одинаковое значение должно быть указано на шлюзе и всех его клиентах.",
|
|
132
|
+
GATEWAY_SCOPE_TOKEN: "Необязательный scope-токен регистрации, выданный шлюзом.",
|
|
133
|
+
TELEGRAM_BOT_TOKEN: "Создайте бота через @BotFather и вставьте полученный токен.",
|
|
134
|
+
MCP_HTTP_BEARER_TOKEN: "Необязательная защита MCP endpoint через Authorization: Bearer.",
|
|
135
|
+
DB_HOST: "PostgreSQL необязателен, но рекомендуется для постоянных collaboration-данных шлюза.",
|
|
136
|
+
TELLYMCP_SESSION_ID: "Стабильный routing ID. Оставьте пустым, чтобы получить его из workspace.",
|
|
137
|
+
TELLYMCP_SESSION_LABEL: "Понятное имя, которое будет показано в списке консолей шлюза.",
|
|
138
|
+
};
|
|
139
|
+
const fieldExamples = {
|
|
140
|
+
PUBLIC_BASE_URL: "https://telly.example.com или https://telly.example.com/api",
|
|
141
|
+
PROJECT_NAME: "backend-api",
|
|
142
|
+
TELLYMCP_SESSION_ID: "backend-dev",
|
|
143
|
+
TELLYMCP_SESSION_LABEL: "Backend · Development",
|
|
144
|
+
TELEGRAM_BOT_TOKEN: "123456789:AA...",
|
|
145
|
+
TELEGRAM_BOT_USERNAME: "my_telly_bot",
|
|
146
|
+
ADMIN_TOKEN: "случайная строка длиной 32+ символа",
|
|
147
|
+
REDIS_HOST: "127.0.0.1 или redis.internal",
|
|
148
|
+
REDIS_PORT: "6379",
|
|
149
|
+
REDIS_DB: "1",
|
|
150
|
+
REDIS_USERNAME: "tellymcp",
|
|
151
|
+
REDIS_PASSWORD: "пароль Redis",
|
|
152
|
+
GATEWAY_AUTH_TOKEN: "один общий случайный секрет для gateway и client",
|
|
153
|
+
GATEWAY_SCOPE_TOKEN: "scope-токен из шлюза",
|
|
154
|
+
GATEWAY_USER_UUID: "550e8400-e29b-41d4-a716-446655440000",
|
|
155
|
+
DB_HOST: "postgres.internal",
|
|
156
|
+
DB_PORT: "5432",
|
|
157
|
+
DB_USER: "tellymcp",
|
|
158
|
+
DB_PASSWORD: "пароль PostgreSQL",
|
|
159
|
+
DB_NAME: "tellymcp",
|
|
160
|
+
DB_SCHEMA: "mcp",
|
|
161
|
+
MCP_HTTP_HOST: "127.0.0.1",
|
|
162
|
+
MCP_HTTP_PORT: "8787",
|
|
163
|
+
MCP_HTTP_PATH: "/mcp",
|
|
164
|
+
MCP_HTTP_BEARER_TOKEN: "случайный bearer-секрет",
|
|
165
|
+
ROOT_PREFIX: "/api",
|
|
166
|
+
PORT: "8080",
|
|
167
|
+
TELLYMCP_MAGIC_TOKEN: "случайная строка длиной 32+ символа",
|
|
168
|
+
TELLYMCP_MAGIC_TOKEN_HASH: "sha256:<64 hex-символа>",
|
|
169
|
+
TELLYMCP_OAUTH_CLIENT_ID: "tellymcp",
|
|
170
|
+
TELLYMCP_ALLOWED_REDIRECT_URIS: "https://chatgpt.com/connector/oauth/callback,https://claude.ai/api/mcp/auth_callback",
|
|
171
|
+
RMQ_HOST: "rabbitmq.internal",
|
|
172
|
+
RMQ_PORT: "5672",
|
|
173
|
+
RMQ_VHOST: "/",
|
|
174
|
+
RMQ_EXCHANGE: "telegram_mcp.gateway",
|
|
175
|
+
TERMINAL_SHELL: "bash",
|
|
176
|
+
BROWSER_ADDRESS: "http://localhost:5173",
|
|
177
|
+
BROWSER_EXECUTABLE_PATH: "/usr/bin/google-chrome",
|
|
178
|
+
BROWSER_ATTACH_WS_HOST: "127.0.0.1",
|
|
179
|
+
BROWSER_ATTACH_WS_PORT: "9999",
|
|
180
|
+
BROWSER_ATTACH_WS_PATH: "/browser-attach/ws",
|
|
181
|
+
NAMESPACE: "mcp",
|
|
182
|
+
NODE_ID: "agent-backend",
|
|
183
|
+
LOG_FILE_PATH: ".tellymcp/log.jsonl",
|
|
184
|
+
};
|
|
185
|
+
function buildFieldHelp(input) {
|
|
186
|
+
const example = fieldExamples[input.key] ??
|
|
187
|
+
(input.value && input.value.length <= 60 ? input.value : undefined);
|
|
188
|
+
const baseHelp = input.derived
|
|
189
|
+
? "Значение рассчитывается автоматически из публичного базового URL."
|
|
190
|
+
: (fieldHelp[input.key] ??
|
|
191
|
+
(isSecretKey(input.key)
|
|
192
|
+
? "Секретное значение. Не публикуйте его и не отправляйте в чат."
|
|
193
|
+
: input.kind === "number"
|
|
194
|
+
? "Укажите целое число; рекомендуемое значение уже подставлено."
|
|
195
|
+
: input.kind === "select"
|
|
196
|
+
? "Выберите подходящий режим из списка."
|
|
197
|
+
: input.kind === "url"
|
|
198
|
+
? "Укажите абсолютный URL вместе с протоколом."
|
|
199
|
+
: "Оставьте рекомендуемое значение или измените его под своё окружение."));
|
|
200
|
+
return {
|
|
201
|
+
help: example ? `${baseHelp} Пример: ${example}.` : baseHelp,
|
|
202
|
+
...(example ? { placeholder: example } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const selectOptions = {
|
|
206
|
+
TELEGRAM_REQUEST_MODE: ["reject", "queue"],
|
|
207
|
+
DEBUG_LANGUAGE: ["ru", "en"],
|
|
208
|
+
WEBAPP_LAUNCH_MODE: ["fullscreen", "expand", "default"],
|
|
209
|
+
TERMINAL_CAPTURE_MODE: ["visible", "lines"],
|
|
210
|
+
TERMINAL_PROMPT_SCAN_STRATEGY: ["strict", "balanced"],
|
|
211
|
+
BROWSER_WAIT_UNTIL: ["load", "domcontentloaded", "networkidle", "commit"],
|
|
212
|
+
BROWSER_CHANNEL: ["", "chrome", "chromium", "msedge"],
|
|
213
|
+
PROXY_USE: ["", "http", "socks5"],
|
|
214
|
+
LOG_LEVEL: ["fatal", "error", "warn", "info", "debug", "trace", "silent"],
|
|
215
|
+
LOG_STDERR_LEVEL: [
|
|
216
|
+
"",
|
|
217
|
+
"fatal",
|
|
218
|
+
"error",
|
|
219
|
+
"warn",
|
|
220
|
+
"info",
|
|
221
|
+
"debug",
|
|
222
|
+
"trace",
|
|
223
|
+
"silent",
|
|
224
|
+
],
|
|
225
|
+
LOG_FILE_LEVEL: [
|
|
226
|
+
"",
|
|
227
|
+
"fatal",
|
|
228
|
+
"error",
|
|
229
|
+
"warn",
|
|
230
|
+
"info",
|
|
231
|
+
"debug",
|
|
232
|
+
"trace",
|
|
233
|
+
"silent",
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
const booleanKeys = new Set([
|
|
237
|
+
"OAUTH_ENABLED",
|
|
238
|
+
"TELEGRAM_WEBHOOK_ENABLED",
|
|
239
|
+
"TELEGRAM_WEBHOOK_TRACE",
|
|
240
|
+
"TELEGRAM_WEBHOOK_DROP_PENDING_UPDATES",
|
|
241
|
+
"MCP_HTTP_ENABLE_DEBUG_ROUTES",
|
|
242
|
+
"MCP_HTTP_ENABLE_PRUNE_ROUTE",
|
|
243
|
+
"WEBAPP_ENABLED",
|
|
244
|
+
"TERMINAL_NUDGE_ENABLED",
|
|
245
|
+
"TERMINAL_PROMPT_SCAN_ENABLED",
|
|
246
|
+
"BROWSER_ENABLED",
|
|
247
|
+
"BROWSER_HEADLESS",
|
|
248
|
+
"BROWSER_DEVTOOLS",
|
|
249
|
+
"BROWSER_ATTACH_ENABLED",
|
|
250
|
+
"MOLECULER_TRACE",
|
|
251
|
+
"MOLECULER_METRICS",
|
|
252
|
+
"LOG_FILE_ENABLED",
|
|
253
|
+
]);
|
|
254
|
+
function sectionForKey(key) {
|
|
255
|
+
if (key === "PUBLIC_BASE_URL")
|
|
256
|
+
return "Быстрая настройка";
|
|
257
|
+
if (key.startsWith("TELEGRAM_") ||
|
|
258
|
+
key === "ADMIN_TOKEN" ||
|
|
259
|
+
key === "DEBUG_LANGUAGE") {
|
|
260
|
+
return "Telegram";
|
|
261
|
+
}
|
|
262
|
+
if (key.startsWith("REDIS_"))
|
|
263
|
+
return "Redis";
|
|
264
|
+
if (key.startsWith("GATEWAY_"))
|
|
265
|
+
return "Подключение к шлюзу";
|
|
266
|
+
if (key.startsWith("DB_"))
|
|
267
|
+
return "PostgreSQL";
|
|
268
|
+
if (key.startsWith("TELLYMCP_OAUTH_") ||
|
|
269
|
+
key.startsWith("TELLYMCP_MAGIC_") ||
|
|
270
|
+
key === "TELLYMCP_PUBLIC_URL")
|
|
271
|
+
return "OAuth-коннектор";
|
|
272
|
+
if (key.startsWith("WEBAPP_"))
|
|
273
|
+
return "Telegram Mini App";
|
|
274
|
+
if (key.startsWith("TERMINAL_"))
|
|
275
|
+
return "Терминал";
|
|
276
|
+
if (key.startsWith("BROWSER_"))
|
|
277
|
+
return "Браузер";
|
|
278
|
+
if (key.startsWith("RMQ_"))
|
|
279
|
+
return "RabbitMQ";
|
|
280
|
+
if (key.startsWith("MCP_") || key === "ROOT_PREFIX" || key === "PORT")
|
|
281
|
+
return "HTTP и MCP";
|
|
282
|
+
if (key === "PROJECT_NAME" || key.startsWith("TELLYMCP_SESSION_"))
|
|
283
|
+
return "Идентификация консоли";
|
|
284
|
+
if (key.startsWith("LOG") ||
|
|
285
|
+
key === "NAMESPACE" ||
|
|
286
|
+
key === "NODE_ID" ||
|
|
287
|
+
key === "TRANSPORT" ||
|
|
288
|
+
key.startsWith("MOLECULER_") ||
|
|
289
|
+
key.startsWith("METRICS_"))
|
|
290
|
+
return "Runtime и логирование";
|
|
291
|
+
if (key === "PROXY_USE" || key.endsWith("_PROXY"))
|
|
292
|
+
return "Прокси";
|
|
293
|
+
return "Дополнительно";
|
|
294
|
+
}
|
|
295
|
+
function labelForKey(key) {
|
|
296
|
+
return fieldLabels[key] ?? `Дополнительный параметр ${key}`;
|
|
297
|
+
}
|
|
298
|
+
function isSecretKey(key) {
|
|
299
|
+
return /(?:TOKEN|PASSWORD|SECRET|PRIVATE_KEY)/u.test(key);
|
|
300
|
+
}
|
|
301
|
+
function fieldKind(key) {
|
|
302
|
+
if (selectOptions[key] || booleanKeys.has(key))
|
|
303
|
+
return "select";
|
|
304
|
+
if (isSecretKey(key))
|
|
305
|
+
return key.endsWith("PRIVATE_KEY_PEM") ? "textarea" : "password";
|
|
306
|
+
if (key.endsWith("_MESSAGE") || key === "TELLYMCP_ALLOWED_REDIRECT_URIS")
|
|
307
|
+
return "textarea";
|
|
308
|
+
if (key.endsWith("_URL") || key === "BROWSER_ADDRESS")
|
|
309
|
+
return "url";
|
|
310
|
+
if (/(?:_PORT|_MS|_SECONDS|_LINES|_COLS|_ROWS|_SIZE|_SCORE|_DB)$/u.test(key) ||
|
|
311
|
+
key === "PORT")
|
|
312
|
+
return "number";
|
|
313
|
+
return "text";
|
|
314
|
+
}
|
|
315
|
+
function requiredKeys(role) {
|
|
316
|
+
const keys = new Set(["PUBLIC_BASE_URL"]);
|
|
317
|
+
if (role === "gateway") {
|
|
318
|
+
keys.add("REDIS_HOST");
|
|
319
|
+
keys.add("REDIS_PORT");
|
|
320
|
+
keys.add("REDIS_DB");
|
|
321
|
+
keys.add("TELEGRAM_BOT_TOKEN");
|
|
322
|
+
keys.add("GATEWAY_AUTH_TOKEN");
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
keys.add("GATEWAY_AUTH_TOKEN");
|
|
326
|
+
}
|
|
327
|
+
return keys;
|
|
328
|
+
}
|
|
329
|
+
function parseTemplateKeys(template) {
|
|
330
|
+
const keys = [];
|
|
331
|
+
for (const line of template.split(/\r?\n/u)) {
|
|
332
|
+
const match = line.match(/^\s*(?:#\s*)?([A-Z][A-Z0-9_]*)=/u);
|
|
333
|
+
if (match?.[1] &&
|
|
334
|
+
match[1] !== "DISTRIBUTED_MODE" &&
|
|
335
|
+
!keys.includes(match[1])) {
|
|
336
|
+
keys.push(match[1]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return keys;
|
|
340
|
+
}
|
|
341
|
+
function buildFields(role, template) {
|
|
342
|
+
const defaults = (0, dotenv_1.parse)(template);
|
|
343
|
+
const values = {
|
|
344
|
+
...defaults,
|
|
345
|
+
DISTRIBUTED_MODE: role,
|
|
346
|
+
};
|
|
347
|
+
const required = requiredKeys(role);
|
|
348
|
+
const derivedKeys = new Set(role === "gateway"
|
|
349
|
+
? [
|
|
350
|
+
"GATEWAY_PUBLIC_URL",
|
|
351
|
+
"GATEWAY_WS_URL",
|
|
352
|
+
"GATEWAY_WS_PATH",
|
|
353
|
+
"ROOT_PREFIX",
|
|
354
|
+
"WEBAPP_PUBLIC_URL",
|
|
355
|
+
"TELEGRAM_WEBHOOK_PUBLIC_URL",
|
|
356
|
+
"TELLYMCP_PUBLIC_URL",
|
|
357
|
+
"TELLYMCP_OAUTH_ISSUER",
|
|
358
|
+
"TELLYMCP_OAUTH_AUDIENCE",
|
|
359
|
+
]
|
|
360
|
+
: ["GATEWAY_PUBLIC_URL", "GATEWAY_WS_URL", "GATEWAY_WS_PATH"]);
|
|
361
|
+
const fields = parseTemplateKeys(template).map((key) => {
|
|
362
|
+
const value = values[key] ?? "";
|
|
363
|
+
const kind = fieldKind(key);
|
|
364
|
+
const derived = derivedKeys.has(key);
|
|
365
|
+
const presentation = buildFieldHelp({ key, kind, value, derived });
|
|
366
|
+
return {
|
|
367
|
+
key,
|
|
368
|
+
label: labelForKey(key),
|
|
369
|
+
section: sectionForKey(key),
|
|
370
|
+
value,
|
|
371
|
+
kind,
|
|
372
|
+
...(booleanKeys.has(key)
|
|
373
|
+
? {
|
|
374
|
+
options: value.trim() ? ["true", "false"] : ["", "true", "false"],
|
|
375
|
+
}
|
|
376
|
+
: selectOptions[key]
|
|
377
|
+
? {
|
|
378
|
+
options: value.trim() || selectOptions[key].includes("")
|
|
379
|
+
? selectOptions[key]
|
|
380
|
+
: ["", ...selectOptions[key]],
|
|
381
|
+
}
|
|
382
|
+
: {}),
|
|
383
|
+
required: required.has(key),
|
|
384
|
+
...(derived ? { derived: true } : {}),
|
|
385
|
+
...presentation,
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
const publicBasePresentation = buildFieldHelp({
|
|
389
|
+
key: "PUBLIC_BASE_URL",
|
|
390
|
+
kind: "url",
|
|
391
|
+
value: "",
|
|
392
|
+
derived: false,
|
|
393
|
+
});
|
|
394
|
+
fields.unshift({
|
|
395
|
+
key: "PUBLIC_BASE_URL",
|
|
396
|
+
label: fieldLabels.PUBLIC_BASE_URL,
|
|
397
|
+
section: "Быстрая настройка",
|
|
398
|
+
value: "",
|
|
399
|
+
kind: "url",
|
|
400
|
+
required: true,
|
|
401
|
+
...publicBasePresentation,
|
|
402
|
+
});
|
|
403
|
+
if (role === "gateway") {
|
|
404
|
+
const oauthIndex = fields.findIndex((field) => field.section === "OAuth-коннектор");
|
|
405
|
+
fields.splice(oauthIndex < 0 ? fields.length : oauthIndex, 0, {
|
|
406
|
+
key: "OAUTH_ENABLED",
|
|
407
|
+
label: fieldLabels.OAUTH_ENABLED,
|
|
408
|
+
section: "OAuth-коннектор",
|
|
409
|
+
value: "false",
|
|
410
|
+
kind: "select",
|
|
411
|
+
options: ["false", "true"],
|
|
412
|
+
required: false,
|
|
413
|
+
...buildFieldHelp({
|
|
414
|
+
key: "OAUTH_ENABLED",
|
|
415
|
+
kind: "select",
|
|
416
|
+
value: "false",
|
|
417
|
+
derived: false,
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return fields;
|
|
422
|
+
}
|
|
423
|
+
function normalizePath(value) {
|
|
424
|
+
const normalized = `/${value}`.replace(/\/{2,}/gu, "/").replace(/\/+$/u, "");
|
|
425
|
+
return normalized === "/" ? "" : normalized;
|
|
426
|
+
}
|
|
427
|
+
function deriveEnvironmentFromPublicBase(input) {
|
|
428
|
+
const values = { ...input.values };
|
|
429
|
+
const rawBase = values.PUBLIC_BASE_URL?.trim();
|
|
430
|
+
if (!rawBase)
|
|
431
|
+
return values;
|
|
432
|
+
let url;
|
|
433
|
+
try {
|
|
434
|
+
url = new URL(rawBase);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return values;
|
|
438
|
+
}
|
|
439
|
+
let rootPath = normalizePath(url.pathname);
|
|
440
|
+
const knownSuffixes = [
|
|
441
|
+
"/telegram/webhook",
|
|
442
|
+
"/gateway/ws",
|
|
443
|
+
"/gateway",
|
|
444
|
+
"/webapp",
|
|
445
|
+
"/mcp",
|
|
446
|
+
];
|
|
447
|
+
for (const suffix of knownSuffixes) {
|
|
448
|
+
if (rootPath.endsWith(suffix)) {
|
|
449
|
+
rootPath = normalizePath(rootPath.slice(0, -suffix.length));
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!rootPath) {
|
|
454
|
+
rootPath = normalizePath(values.ROOT_PREFIX?.trim() || "/api");
|
|
455
|
+
}
|
|
456
|
+
const httpBase = `${url.origin}${rootPath}`;
|
|
457
|
+
const wsProtocol = url.protocol === "http:" ? "ws:" : "wss:";
|
|
458
|
+
const wsBase = `${wsProtocol}//${url.host}${rootPath}`;
|
|
459
|
+
values.PUBLIC_BASE_URL = httpBase;
|
|
460
|
+
values.GATEWAY_PUBLIC_URL = `${httpBase}/gateway`;
|
|
461
|
+
values.GATEWAY_WS_URL = `${wsBase}/gateway/ws`;
|
|
462
|
+
values.GATEWAY_WS_PATH = `${rootPath}/gateway/ws`;
|
|
463
|
+
if (input.role === "gateway") {
|
|
464
|
+
values.ROOT_PREFIX = rootPath || "/";
|
|
465
|
+
values.WEBAPP_PUBLIC_URL = `${httpBase}/webapp`;
|
|
466
|
+
values.TELEGRAM_WEBHOOK_PUBLIC_URL = `${httpBase}/telegram/webhook`;
|
|
467
|
+
if (values.OAUTH_ENABLED === "true") {
|
|
468
|
+
values.TELLYMCP_PUBLIC_URL = httpBase;
|
|
469
|
+
values.TELLYMCP_OAUTH_ISSUER = httpBase;
|
|
470
|
+
values.TELLYMCP_OAUTH_AUDIENCE = httpBase;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
values.TELLYMCP_PUBLIC_URL = "";
|
|
474
|
+
values.TELLYMCP_OAUTH_ISSUER = "";
|
|
475
|
+
values.TELLYMCP_OAUTH_AUDIENCE = "";
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return values;
|
|
479
|
+
}
|
|
480
|
+
function formatEnvironmentValue(value) {
|
|
481
|
+
if (/^[A-Za-z0-9_./:@%+,?&=-]*$/u.test(value))
|
|
482
|
+
return value;
|
|
483
|
+
return JSON.stringify(value);
|
|
484
|
+
}
|
|
485
|
+
function renderConfiguredEnvironment(input) {
|
|
486
|
+
const allowedKeys = new Set(parseTemplateKeys(input.template));
|
|
487
|
+
const lines = [
|
|
488
|
+
"# Generated by tellymcp configure.",
|
|
489
|
+
`# Role: ${input.role === "gateway" ? "gateway" : "client"}`,
|
|
490
|
+
"",
|
|
491
|
+
`DISTRIBUTED_MODE=${input.role}`,
|
|
492
|
+
"",
|
|
493
|
+
];
|
|
494
|
+
let lastSection = "";
|
|
495
|
+
for (const key of parseTemplateKeys(input.template)) {
|
|
496
|
+
if (!allowedKeys.has(key))
|
|
497
|
+
continue;
|
|
498
|
+
const value = input.values[key]?.trim() ?? "";
|
|
499
|
+
if (!value)
|
|
500
|
+
continue;
|
|
501
|
+
const section = sectionForKey(key);
|
|
502
|
+
if (section !== lastSection) {
|
|
503
|
+
if (lastSection)
|
|
504
|
+
lines.push("");
|
|
505
|
+
lines.push(`# ${section}`);
|
|
506
|
+
lastSection = section;
|
|
507
|
+
}
|
|
508
|
+
lines.push(`${key}=${formatEnvironmentValue(value)}`);
|
|
509
|
+
}
|
|
510
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
511
|
+
}
|
|
512
|
+
function validateConfiguredEnvironment(input) {
|
|
513
|
+
const errors = {};
|
|
514
|
+
for (const key of requiredKeys(input.role)) {
|
|
515
|
+
if (!input.values[key]?.trim())
|
|
516
|
+
errors[key] = "Обязательное поле для выбранной роли.";
|
|
517
|
+
}
|
|
518
|
+
for (const [key, value] of Object.entries(input.values)) {
|
|
519
|
+
const trimmed = value.trim();
|
|
520
|
+
if (!trimmed)
|
|
521
|
+
continue;
|
|
522
|
+
if (fieldKind(key) === "number") {
|
|
523
|
+
const number = Number(trimmed);
|
|
524
|
+
if (!Number.isInteger(number) || number < 0)
|
|
525
|
+
errors[key] = "Укажите целое неотрицательное число.";
|
|
526
|
+
if ((key.endsWith("_PORT") || key === "PORT") &&
|
|
527
|
+
(!Number.isInteger(number) || number < 1 || number > 65535))
|
|
528
|
+
errors[key] = "Порт должен быть целым числом от 1 до 65535.";
|
|
529
|
+
}
|
|
530
|
+
if (fieldKind(key) === "url") {
|
|
531
|
+
try {
|
|
532
|
+
const url = new URL(trimmed);
|
|
533
|
+
const allowedProtocols = key === "GATEWAY_WS_URL"
|
|
534
|
+
? new Set(["ws:", "wss:"])
|
|
535
|
+
: new Set(["http:", "https:"]);
|
|
536
|
+
if (!allowedProtocols.has(url.protocol)) {
|
|
537
|
+
errors[key] =
|
|
538
|
+
`Допустимые протоколы: ${[...allowedProtocols].join(", ")}.`;
|
|
539
|
+
}
|
|
540
|
+
if (key === "PUBLIC_BASE_URL" && (url.search || url.hash)) {
|
|
541
|
+
errors[key] =
|
|
542
|
+
"Базовый URL не должен содержать query-параметры или fragment.";
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
errors[key] = "Укажите корректный абсолютный URL.";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const options = booleanKeys.has(key)
|
|
550
|
+
? ["true", "false"]
|
|
551
|
+
: selectOptions[key];
|
|
552
|
+
if (options && !options.includes(trimmed)) {
|
|
553
|
+
errors[key] = "Выберите одно из поддерживаемых значений.";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (input.values.TELEGRAM_WEBHOOK_ENABLED === "true") {
|
|
557
|
+
if (!input.values.TELEGRAM_WEBHOOK_PUBLIC_URL?.trim())
|
|
558
|
+
errors.TELEGRAM_WEBHOOK_PUBLIC_URL =
|
|
559
|
+
"Поле обязательно, когда включён webhook.";
|
|
560
|
+
if (!input.values.TELEGRAM_WEBHOOK_SECRET?.trim())
|
|
561
|
+
errors.TELEGRAM_WEBHOOK_SECRET = "Секрет обязателен для webhook.";
|
|
562
|
+
}
|
|
563
|
+
if (input.values.PROXY_USE === "http" && !input.values.HTTP_PROXY?.trim())
|
|
564
|
+
errors.HTTP_PROXY = "Укажите HTTP proxy для выбранного режима.";
|
|
565
|
+
if (input.values.PROXY_USE === "socks5" && !input.values.SOCKS5_PROXY?.trim())
|
|
566
|
+
errors.SOCKS5_PROXY = "Укажите SOCKS5 proxy для выбранного режима.";
|
|
567
|
+
const oauthConfigured = [
|
|
568
|
+
"TELLYMCP_PUBLIC_URL",
|
|
569
|
+
"TELLYMCP_OAUTH_ISSUER",
|
|
570
|
+
"TELLYMCP_OAUTH_AUDIENCE",
|
|
571
|
+
"TELLYMCP_MAGIC_TOKEN",
|
|
572
|
+
"TELLYMCP_MAGIC_TOKEN_HASH",
|
|
573
|
+
"TELLYMCP_OAUTH_CLIENT_ID",
|
|
574
|
+
"TELLYMCP_OAUTH_CLIENT_SECRET",
|
|
575
|
+
"TELLYMCP_ALLOWED_REDIRECT_URIS",
|
|
576
|
+
"TELLYMCP_OAUTH_PRIVATE_KEY_PEM",
|
|
577
|
+
].some((key) => input.values[key]?.trim());
|
|
578
|
+
if (oauthConfigured && !input.values.TELLYMCP_PUBLIC_URL?.trim()) {
|
|
579
|
+
errors.TELLYMCP_PUBLIC_URL =
|
|
580
|
+
"Поле обязательно, когда настроен OAuth-коннектор.";
|
|
581
|
+
}
|
|
582
|
+
if (oauthConfigured &&
|
|
583
|
+
!input.values.TELLYMCP_MAGIC_TOKEN?.trim() &&
|
|
584
|
+
!input.values.TELLYMCP_MAGIC_TOKEN_HASH?.trim()) {
|
|
585
|
+
errors.TELLYMCP_MAGIC_TOKEN = "Укажите magic token или его SHA-256 hash.";
|
|
586
|
+
}
|
|
587
|
+
if (input.values.TELLYMCP_MAGIC_TOKEN?.trim() &&
|
|
588
|
+
input.values.TELLYMCP_MAGIC_TOKEN_HASH?.trim()) {
|
|
589
|
+
errors.TELLYMCP_MAGIC_TOKEN_HASH =
|
|
590
|
+
"Укажите либо обычный token, либо hash — не оба сразу.";
|
|
591
|
+
}
|
|
592
|
+
if (input.values.TELLYMCP_MAGIC_TOKEN_HASH?.trim() &&
|
|
593
|
+
!/^sha256:[a-f0-9]{64}$/iu.test(input.values.TELLYMCP_MAGIC_TOKEN_HASH.trim())) {
|
|
594
|
+
errors.TELLYMCP_MAGIC_TOKEN_HASH =
|
|
595
|
+
"Используйте формат sha256:<64 шестнадцатеричных символа>.";
|
|
596
|
+
}
|
|
597
|
+
if (input.values.TELLYMCP_OAUTH_CLIENT_SECRET?.trim() &&
|
|
598
|
+
!input.values.TELLYMCP_OAUTH_CLIENT_ID?.trim()) {
|
|
599
|
+
errors.TELLYMCP_OAUTH_CLIENT_ID =
|
|
600
|
+
"Client ID обязателен, когда указан OAuth client secret.";
|
|
601
|
+
}
|
|
602
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
603
|
+
}
|
|
604
|
+
function sanitizeConnectionError(error, values) {
|
|
605
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
606
|
+
for (const [key, value] of Object.entries(values)) {
|
|
607
|
+
if (!value || !isSecretKey(key))
|
|
608
|
+
continue;
|
|
609
|
+
message = message.split(value).join("[СКРЫТО]");
|
|
610
|
+
}
|
|
611
|
+
return (message.replace(/\s+/gu, " ").trim().slice(0, 300) || "Неизвестная ошибка");
|
|
612
|
+
}
|
|
613
|
+
async function checkTelegramBot(values) {
|
|
614
|
+
const token = values.TELEGRAM_BOT_TOKEN?.trim();
|
|
615
|
+
if (!token)
|
|
616
|
+
return { ok: false, message: "Сначала укажите токен Telegram-бота." };
|
|
617
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
618
|
+
signal: AbortSignal.timeout(5_000),
|
|
619
|
+
});
|
|
620
|
+
const payload = (await response.json());
|
|
621
|
+
if (!response.ok || !payload.ok) {
|
|
622
|
+
throw new Error(payload.description || `Telegram вернул HTTP ${response.status}`);
|
|
623
|
+
}
|
|
624
|
+
const identity = payload.result?.username
|
|
625
|
+
? `@${payload.result.username}`
|
|
626
|
+
: payload.result?.first_name || "без username";
|
|
627
|
+
return {
|
|
628
|
+
ok: true,
|
|
629
|
+
message: `Соединение установлено. Найден бот ${identity}.`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function checkRedis(values) {
|
|
633
|
+
const client = new ioredis_1.default({
|
|
634
|
+
host: values.REDIS_HOST?.trim() || "127.0.0.1",
|
|
635
|
+
port: Number(values.REDIS_PORT || 6379),
|
|
636
|
+
db: Number(values.REDIS_DB || 0),
|
|
637
|
+
...(values.REDIS_USERNAME?.trim()
|
|
638
|
+
? { username: values.REDIS_USERNAME.trim() }
|
|
639
|
+
: {}),
|
|
640
|
+
...(values.REDIS_PASSWORD ? { password: values.REDIS_PASSWORD } : {}),
|
|
641
|
+
lazyConnect: true,
|
|
642
|
+
connectTimeout: 4_000,
|
|
643
|
+
maxRetriesPerRequest: 0,
|
|
644
|
+
retryStrategy: () => null,
|
|
645
|
+
});
|
|
646
|
+
client.on("error", () => undefined);
|
|
647
|
+
try {
|
|
648
|
+
await client.connect();
|
|
649
|
+
const reply = await client.ping();
|
|
650
|
+
if (reply !== "PONG")
|
|
651
|
+
throw new Error(`Неожиданный ответ Redis: ${reply}`);
|
|
652
|
+
return { ok: true, message: "Redis доступен и ответил PONG." };
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
client.disconnect();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function checkPostgres(values) {
|
|
659
|
+
if (!values.DB_HOST?.trim()) {
|
|
660
|
+
return { ok: false, message: "Сначала укажите хост PostgreSQL." };
|
|
661
|
+
}
|
|
662
|
+
const client = new pg_1.Client({
|
|
663
|
+
host: values.DB_HOST.trim(),
|
|
664
|
+
port: Number(values.DB_PORT || 5432),
|
|
665
|
+
user: values.DB_USER?.trim() || undefined,
|
|
666
|
+
password: values.DB_PASSWORD || undefined,
|
|
667
|
+
database: values.DB_NAME?.trim() || undefined,
|
|
668
|
+
connectionTimeoutMillis: 4_000,
|
|
669
|
+
});
|
|
670
|
+
try {
|
|
671
|
+
await client.connect();
|
|
672
|
+
await client.query("SELECT 1");
|
|
673
|
+
return {
|
|
674
|
+
ok: true,
|
|
675
|
+
message: "PostgreSQL доступен; запрос SELECT 1 выполнен.",
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
await client.end().catch(() => undefined);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function checkGatewayWebSocket(values) {
|
|
683
|
+
await new Promise((resolve, reject) => {
|
|
684
|
+
const socket = new ws_1.default(values.GATEWAY_WS_URL, {
|
|
685
|
+
headers: {
|
|
686
|
+
Authorization: `Bearer ${values.GATEWAY_AUTH_TOKEN}`,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
const timer = setTimeout(() => {
|
|
690
|
+
socket.terminate();
|
|
691
|
+
reject(new Error("WebSocket handshake не завершился за 5 секунд."));
|
|
692
|
+
}, 5_000);
|
|
693
|
+
const finish = (error) => {
|
|
694
|
+
clearTimeout(timer);
|
|
695
|
+
socket.removeAllListeners();
|
|
696
|
+
if (error)
|
|
697
|
+
reject(error);
|
|
698
|
+
else
|
|
699
|
+
resolve();
|
|
700
|
+
};
|
|
701
|
+
socket.once("open", () => {
|
|
702
|
+
socket.close();
|
|
703
|
+
finish();
|
|
704
|
+
});
|
|
705
|
+
socket.once("unexpected-response", (_request, response) => {
|
|
706
|
+
socket.terminate();
|
|
707
|
+
finish(new Error(`WebSocket endpoint вернул HTTP ${response.statusCode}.`));
|
|
708
|
+
});
|
|
709
|
+
socket.once("error", (error) => finish(error));
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async function checkGateway(values) {
|
|
713
|
+
if (!values.GATEWAY_AUTH_TOKEN?.trim()) {
|
|
714
|
+
return {
|
|
715
|
+
ok: false,
|
|
716
|
+
message: "Сначала укажите общий transport-токен шлюза.",
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const base = values.PUBLIC_BASE_URL?.replace(/\/+$/u, "");
|
|
720
|
+
if (!base || !values.GATEWAY_WS_URL) {
|
|
721
|
+
return { ok: false, message: "Сначала укажите публичный базовый URL." };
|
|
722
|
+
}
|
|
723
|
+
const response = await fetch(`${base}/healthz`, {
|
|
724
|
+
signal: AbortSignal.timeout(5_000),
|
|
725
|
+
});
|
|
726
|
+
if (!response.ok)
|
|
727
|
+
throw new Error(`Gateway health вернул HTTP ${response.status}.`);
|
|
728
|
+
await checkGatewayWebSocket(values);
|
|
729
|
+
return {
|
|
730
|
+
ok: true,
|
|
731
|
+
message: "Gateway health доступен, WebSocket handshake успешно выполнен.",
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async function checkRabbitMq(values) {
|
|
735
|
+
if (!values.RMQ_HOST?.trim()) {
|
|
736
|
+
return { ok: false, message: "Сначала укажите хост RabbitMQ." };
|
|
737
|
+
}
|
|
738
|
+
const connection = await (0, amqplib_1.connect)({
|
|
739
|
+
protocol: "amqp",
|
|
740
|
+
hostname: values.RMQ_HOST.trim(),
|
|
741
|
+
port: Number(values.RMQ_PORT || 5672),
|
|
742
|
+
username: values.RMQ_USER?.trim() || "guest",
|
|
743
|
+
password: values.RMQ_PASSWORD || "guest",
|
|
744
|
+
vhost: values.RMQ_VHOST?.trim() || "/",
|
|
745
|
+
}, { timeout: 5_000 });
|
|
746
|
+
try {
|
|
747
|
+
return { ok: true, message: "Соединение с RabbitMQ установлено." };
|
|
748
|
+
}
|
|
749
|
+
finally {
|
|
750
|
+
await connection.close();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function runConfigureConnectionCheck(input) {
|
|
754
|
+
const values = deriveEnvironmentFromPublicBase({
|
|
755
|
+
role: input.role,
|
|
756
|
+
values: input.values,
|
|
757
|
+
});
|
|
758
|
+
try {
|
|
759
|
+
if (input.kind === "telegram")
|
|
760
|
+
return await checkTelegramBot(values);
|
|
761
|
+
if (input.kind === "redis")
|
|
762
|
+
return await checkRedis(values);
|
|
763
|
+
if (input.kind === "postgres")
|
|
764
|
+
return await checkPostgres(values);
|
|
765
|
+
if (input.kind === "gateway")
|
|
766
|
+
return await checkGateway(values);
|
|
767
|
+
return await checkRabbitMq(values);
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
return {
|
|
771
|
+
ok: false,
|
|
772
|
+
message: `Проверка не пройдена: ${sanitizeConnectionError(error, values)}`,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function writeJson(response, status, body) {
|
|
777
|
+
response.writeHead(status, {
|
|
778
|
+
"content-type": "application/json; charset=utf-8",
|
|
779
|
+
"cache-control": "no-store",
|
|
780
|
+
});
|
|
781
|
+
response.end(JSON.stringify(body));
|
|
782
|
+
}
|
|
783
|
+
async function readJsonBody(request) {
|
|
784
|
+
const chunks = [];
|
|
785
|
+
let size = 0;
|
|
786
|
+
for await (const chunk of request) {
|
|
787
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
788
|
+
size += buffer.length;
|
|
789
|
+
if (size > 1_000_000)
|
|
790
|
+
throw new Error("Request body is too large.");
|
|
791
|
+
chunks.push(buffer);
|
|
792
|
+
}
|
|
793
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
794
|
+
}
|
|
795
|
+
function renderHtml(input) {
|
|
796
|
+
const state = JSON.stringify(input).replace(/</gu, "\\u003c");
|
|
797
|
+
return `<!doctype html>
|
|
798
|
+
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
799
|
+
<title>Настройка TellyMCP</title><style nonce="${input.nonce}">
|
|
800
|
+
:root{color-scheme:dark;--bg:#071018;--panel:#0f1b25;--line:#223444;--text:#edf7ff;--muted:#8fa6b8;--cyan:#45d7ff;--violet:#a78bfa;--red:#ff6b7a;--green:#4ade80}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 10% 0,#12314a 0,transparent 34%),radial-gradient(circle at 90% 10%,#27184c 0,transparent 30%),var(--bg);font:15px/1.45 Inter,ui-sans-serif,system-ui;color:var(--text)}main{max-width:1120px;margin:auto;padding:48px 24px 96px}.brand{font-size:13px;letter-spacing:.18em;text-transform:uppercase;color:var(--cyan);font-weight:800}.hero h1{font-size:clamp(32px,5vw,60px);line-height:1;margin:12px 0}.hero p{color:var(--muted);max-width:760px}.role-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin-top:34px}.role-card{display:block;text-align:left;padding:28px;border:1px solid var(--line);background:linear-gradient(145deg,#102532,#101725);color:var(--text);border-radius:20px;cursor:pointer}.role-card:hover{border-color:var(--cyan);transform:translateY(-2px)}.role-card strong{display:block;font-size:25px;margin-bottom:8px}.role-card span{color:var(--muted)}.wizard-head{display:flex;align-items:center;gap:14px;margin:28px 0 18px}.wizard-head h2{font-size:28px;margin:0}.pill{padding:7px 12px;border:1px solid #39546a;border-radius:999px;text-transform:uppercase;font-weight:800;color:var(--violet)}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.section{grid-column:1/-1;background:linear-gradient(145deg,#10202cdd,#0d1720ee);border:1px solid var(--line);border-radius:18px;padding:22px;box-shadow:0 18px 60px #0005}.section-title{display:flex;align-items:center;gap:12px;margin-bottom:18px}.section-title h3{margin:0;font-size:18px}.section-title .check-button{margin-left:auto;padding:8px 12px;background:#173140;color:var(--cyan);border:1px solid #2c5268}.check-result{font-size:12px;color:var(--muted)}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.field{display:flex;flex-direction:column;gap:7px}.field.wide{grid-column:1/-1}label{font-weight:700}.key{font:11px ui-monospace,SFMono-Regular,monospace;color:var(--muted);font-weight:400;margin-left:6px}.required{color:var(--cyan)}input,select,textarea{width:100%;border:1px solid #2b4152;background:#08121a;color:var(--text);border-radius:10px;padding:11px 12px;font:inherit;outline:none}input:focus,select:focus,textarea:focus{border-color:var(--cyan);box-shadow:0 0 0 3px #45d7ff22}input[readonly],textarea[readonly]{color:#9dc8d8;background:#0b1821;border-style:dashed}textarea{min-height:92px;resize:vertical}.help{font-size:12px;color:var(--muted)}.error{font-size:12px;color:var(--red);min-height:17px}.actions{position:sticky;bottom:16px;margin-top:24px;background:#0b1720ed;border:1px solid var(--line);backdrop-filter:blur(16px);border-radius:18px;padding:16px;display:flex;align-items:center;gap:12px;box-shadow:0 20px 50px #0008}button{border:0;border-radius:11px;padding:12px 18px;font:inherit;font-weight:800;cursor:pointer}.primary{background:linear-gradient(100deg,var(--cyan),var(--violet));color:#071018}.secondary{background:#172735;color:var(--text)}button:disabled{opacity:.55;cursor:wait}.status{margin-left:auto;color:var(--muted)}.ok{color:var(--green)}.bad{color:var(--red)}.notice{margin-top:18px;color:var(--muted);border-left:3px solid var(--violet);padding-left:12px}[hidden]{display:none!important}@media(max-width:760px){main{padding:28px 14px 80px}.role-grid,.fields{grid-template-columns:1fr}.field.wide{grid-column:auto}.actions{flex-wrap:wrap}.status{width:100%;margin:0}.section{padding:17px}.section-title{align-items:flex-start;flex-wrap:wrap}.section-title .check-button{margin-left:0}}
|
|
801
|
+
</style></head><body><main><div class="hero"><div class="brand">Локальная настройка TellyMCP</div><h1>Мастер конфигурации</h1><p>Выберите роль этой машины. Значения обрабатываются локально, а готовый dotenv будет скачан через браузер.</p></div><section id="role-step"><div class="role-grid"><button class="role-card" type="button" data-role="client"><strong>Клиент</strong><span>Консоль агента, подключённая к существующему шлюзу TellyMCP.</span></button><button class="role-card" type="button" data-role="gateway"><strong>Шлюз</strong><span>Telegram-бот, MCP endpoint, маршрутизация и сервисы совместной работы.</span></button></div></section><form id="form" hidden><div class="wizard-head"><button class="secondary" type="button" id="back">Назад</button><h2 id="wizard-title"></h2><span class="pill" id="role-pill"></span></div><div class="grid" id="sections"></div><div class="notice">Браузер скачает файл <b id="filename"></b>. После скачивания выполните <code>chmod 600 <file></code>, потому что браузер не гарантирует Unix-права файла.</div><div class="actions"><button class="secondary" type="button" id="validate">Проверить все поля</button><button class="primary" type="submit">Скачать dotenv</button><div class="status" id="status">Готов к настройке</div></div></form></main><script nonce="${input.nonce}">
|
|
802
|
+
const state=${state};let selected=null;let fields=new Map();const root=document.querySelector('#sections');const form=document.querySelector('#form');const roleStep=document.querySelector('#role-step');const status=document.querySelector('#status');
|
|
803
|
+
function esc(v){return String(v).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}function optionLabel(value){return ({'':'Не задано','true':'Да','false':'Нет','reject':'Отклонять параллельные запросы','queue':'Ставить запросы в очередь','ru':'Русский','en':'English','fullscreen':'На весь экран','expand':'Развернуть','default':'По умолчанию','visible':'Видимая область','lines':'Последние строки','strict':'Строгая','balanced':'Сбалансированная'}[value]||value)}function renderField(f){const wide=f.kind==='textarea'?' wide':'';const req=f.required?'<span class="required"> *</span>':'';const readonly=f.derived?' readonly':'';const placeholder=f.placeholder?' placeholder="'+esc(f.placeholder)+'"':'';let control;if(f.kind==='select'){control='<select id="'+f.key+'">'+f.options.map(o=>'<option value="'+esc(o)+'" '+(o===f.value?'selected':'')+'>'+esc(optionLabel(o))+'</option>').join('')+'</select>'}else if(f.kind==='textarea'){control='<textarea id="'+f.key+'"'+readonly+placeholder+'>'+esc(f.value)+'</textarea>'}else{control='<input id="'+f.key+'" type="'+f.kind+'" value="'+esc(f.value)+'" '+(f.required?'required':'')+readonly+placeholder+' autocomplete="off">'}return '<div class="field'+wide+'"><label for="'+f.key+'">'+esc(f.label)+req+'<span class="key">'+f.key+'</span></label>'+control+(f.help?'<div class="help">'+esc(f.help)+'</div>':'')+'<div class="error" data-error="'+f.key+'"></div></div>'}
|
|
804
|
+
function normalizePath(value){const normalized=('/'+value).replace(new RegExp('/{2,}','g'),'/').replace(new RegExp('/+$'),'');return normalized==='/'?'':normalized}function setValue(key,value){const element=document.getElementById(key);if(element)element.value=value}function updateDerived(){const baseElement=document.getElementById('PUBLIC_BASE_URL');if(!baseElement?.value.trim())return;try{const url=new URL(baseElement.value.trim());let rootPath=normalizePath(url.pathname);for(const suffix of ['/telegram/webhook','/gateway/ws','/gateway','/webapp','/mcp']){if(rootPath.endsWith(suffix)){rootPath=normalizePath(rootPath.slice(0,-suffix.length));break}}if(!rootPath)rootPath=normalizePath(document.getElementById('ROOT_PREFIX')?.value||'/api');const httpBase=url.origin+rootPath;const wsBase=(url.protocol==='http:'?'ws:':'wss:')+'//'+url.host+rootPath;setValue('GATEWAY_PUBLIC_URL',httpBase+'/gateway');setValue('GATEWAY_WS_URL',wsBase+'/gateway/ws');setValue('GATEWAY_WS_PATH',rootPath+'/gateway/ws');if(selected.role==='gateway'){setValue('ROOT_PREFIX',rootPath||'/');setValue('WEBAPP_PUBLIC_URL',httpBase+'/webapp');setValue('TELEGRAM_WEBHOOK_PUBLIC_URL',httpBase+'/telegram/webhook');const oauth=document.getElementById('OAUTH_ENABLED')?.value==='true';setValue('TELLYMCP_PUBLIC_URL',oauth?httpBase:'');setValue('TELLYMCP_OAUTH_ISSUER',oauth?httpBase:'');setValue('TELLYMCP_OAUTH_AUDIENCE',oauth?httpBase:'')}}catch{}}
|
|
805
|
+
function checkForSection(name,role){if(name==='Telegram'&&role==='gateway')return'telegram';if(name==='Redis'&&role==='gateway')return'redis';if(name==='PostgreSQL'&&role==='gateway')return'postgres';if(name==='Подключение к шлюзу'&&role==='client')return'gateway';if(name==='RabbitMQ'&&role==='gateway')return'rabbitmq';return null}function chooseRole(role){selected=state.roles.find(item=>item.role===role);fields=new Map(selected.fields.map(field=>[field.key,field]));const grouped=new Map();for(const field of selected.fields){if(!grouped.has(field.section))grouped.set(field.section,[]);grouped.get(field.section).push(field)}const essential=new Set(role==='gateway'?['Быстрая настройка','Telegram','Подключение к шлюзу','Redis','PostgreSQL','HTTP и MCP']:['Быстрая настройка','Идентификация консоли','Подключение к шлюзу','HTTP и MCP']);root.innerHTML='';for(const [name,items] of grouped){const check=checkForSection(name,role);const el=document.createElement('section');el.className='section';el.innerHTML='<div class="section-title"><h3>'+esc(name)+(essential.has(name)?'':' · расширенные настройки')+'</h3>'+(check?'<span class="check-result" data-check-result="'+check+'"></span><button class="check-button" type="button" data-check="'+check+'">Проверить соединение</button>':'')+'</div><div class="fields">'+items.map(renderField).join('')+'</div>';root.append(el)}document.querySelector('#wizard-title').textContent='Настройка: '+selected.title;document.querySelector('#role-pill').textContent=selected.role==='gateway'?'шлюз':'клиент';document.querySelector('#filename').textContent=selected.filename;document.getElementById('PUBLIC_BASE_URL').addEventListener('input',updateDerived);document.getElementById('OAUTH_ENABLED')?.addEventListener('change',updateDerived);root.querySelectorAll('[data-check]').forEach(button=>button.addEventListener('click',()=>runCheck(button)));status.className='status';status.textContent='Заполните обязательные поля';roleStep.hidden=true;form.hidden=false;window.scrollTo({top:0,behavior:'smooth'})}
|
|
806
|
+
function values(){return Object.fromEntries([...fields.keys()].map(key=>[key,document.getElementById(key).value]))}function showErrors(errors){document.querySelectorAll('[data-error]').forEach(element=>element.textContent='');for(const [key,message] of Object.entries(errors||{})){const element=document.querySelector('[data-error="'+CSS.escape(key)+'"]');if(element)element.textContent=message}}
|
|
807
|
+
async function post(path,extra={}){const response=await fetch(path+'?token='+encodeURIComponent(state.token),{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({role:selected.role,values:values(),...extra})});return response}async function runCheck(button){const kind=button.dataset.check;const result=document.querySelector('[data-check-result="'+kind+'"]');button.disabled=true;result.className='check-result';result.textContent='Проверяем…';try{const response=await post('/api/check',{check:kind});const data=await response.json();result.className='check-result '+(data.ok?'ok':'bad');result.textContent=data.message}catch(error){result.className='check-result bad';result.textContent='Ошибка проверки: '+error.message}finally{button.disabled=false}}
|
|
808
|
+
document.querySelectorAll('[data-role]').forEach(button=>button.addEventListener('click',()=>chooseRole(button.dataset.role)));document.querySelector('#back').addEventListener('click',()=>{form.hidden=true;roleStep.hidden=false;selected=null;root.innerHTML=''});
|
|
809
|
+
document.querySelector('#validate').addEventListener('click',async()=>{status.textContent='Проверяем поля…';try{const response=await post('/api/validate');const data=await response.json();showErrors(data.errors);status.className='status '+(data.valid?'ok':'bad');status.textContent=data.valid?'Все поля заполнены корректно':'Исправьте подсвеченные поля'}catch(error){status.className='status bad';status.textContent=error.message}});
|
|
810
|
+
form.addEventListener('submit',async event=>{event.preventDefault();status.textContent='Формируем dotenv…';document.querySelectorAll('button').forEach(button=>button.disabled=true);try{const response=await post('/api/download');if(!response.ok){const data=await response.json();showErrors(data.errors);throw new Error(data.message||'Не удалось сформировать dotenv')}const blob=await response.blob();const href=URL.createObjectURL(blob);const link=document.createElement('a');link.href=href;link.download=selected.filename;document.body.append(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(href),1000);showErrors({});status.className='status ok';status.textContent='Файл '+selected.filename+' скачан. Перед запуском выставьте права 0600.'}catch(error){status.className='status bad';status.textContent=error.message;document.querySelectorAll('button').forEach(button=>button.disabled=false)}});
|
|
811
|
+
</script></body></html>`;
|
|
812
|
+
}
|
|
813
|
+
function openBrowser(url) {
|
|
814
|
+
const command = process.platform === "darwin"
|
|
815
|
+
? "open"
|
|
816
|
+
: process.platform === "win32"
|
|
817
|
+
? "cmd"
|
|
818
|
+
: "xdg-open";
|
|
819
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
820
|
+
const child = (0, node_child_process_1.spawn)(command, args, { detached: true, stdio: "ignore" });
|
|
821
|
+
child.unref();
|
|
822
|
+
child.on("error", () => undefined);
|
|
823
|
+
}
|
|
824
|
+
async function startConfigureServer(input) {
|
|
825
|
+
const token = (0, node_crypto_1.randomBytes)(24).toString("base64url");
|
|
826
|
+
const roles = ["client", "gateway"].map((role) => ({
|
|
827
|
+
role,
|
|
828
|
+
title: role === "gateway" ? "Шлюз" : "Клиент",
|
|
829
|
+
filename: role === "gateway" ? ".env-gateway" : ".env-client",
|
|
830
|
+
description: role === "gateway"
|
|
831
|
+
? "Telegram-бот и шлюз маршрутизации"
|
|
832
|
+
: "Консоль агента, подключённая к шлюзу",
|
|
833
|
+
fields: buildFields(role, input.templates[role]),
|
|
834
|
+
}));
|
|
835
|
+
await new Promise((resolve, reject) => {
|
|
836
|
+
const server = (0, node_http_1.createServer)(async (request, response) => {
|
|
837
|
+
try {
|
|
838
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
839
|
+
if (url.searchParams.get("token") !== token) {
|
|
840
|
+
writeJson(response, 403, { message: "Invalid configurator token." });
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
844
|
+
const nonce = (0, node_crypto_1.randomBytes)(16).toString("base64");
|
|
845
|
+
response.writeHead(200, {
|
|
846
|
+
"content-type": "text/html; charset=utf-8",
|
|
847
|
+
"cache-control": "no-store",
|
|
848
|
+
"content-security-policy": `default-src 'none'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; connect-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'`,
|
|
849
|
+
"x-content-type-options": "nosniff",
|
|
850
|
+
"referrer-policy": "no-referrer",
|
|
851
|
+
});
|
|
852
|
+
response.end(renderHtml({
|
|
853
|
+
roles,
|
|
854
|
+
token,
|
|
855
|
+
nonce,
|
|
856
|
+
}));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (request.method === "POST" &&
|
|
860
|
+
(url.pathname === "/api/validate" ||
|
|
861
|
+
url.pathname === "/api/check" ||
|
|
862
|
+
url.pathname === "/api/download")) {
|
|
863
|
+
const body = (await readJsonBody(request));
|
|
864
|
+
const role = body.role === "client" || body.role === "gateway"
|
|
865
|
+
? body.role
|
|
866
|
+
: null;
|
|
867
|
+
if (!role) {
|
|
868
|
+
writeJson(response, 400, {
|
|
869
|
+
message: "Выберите Client или Gateway.",
|
|
870
|
+
});
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (!body.values ||
|
|
874
|
+
typeof body.values !== "object" ||
|
|
875
|
+
Array.isArray(body.values)) {
|
|
876
|
+
writeJson(response, 400, {
|
|
877
|
+
message: "Некорректный набор значений.",
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const roleDefinition = roles.find((item) => item.role === role);
|
|
882
|
+
const allowedKeys = new Set(roleDefinition.fields.map((field) => field.key));
|
|
883
|
+
const submittedValues = Object.fromEntries(Object.entries(body.values)
|
|
884
|
+
.filter(([key]) => allowedKeys.has(key))
|
|
885
|
+
.map(([key, value]) => [
|
|
886
|
+
key,
|
|
887
|
+
typeof value === "string" ? value : String(value ?? ""),
|
|
888
|
+
]));
|
|
889
|
+
const values = deriveEnvironmentFromPublicBase({
|
|
890
|
+
role,
|
|
891
|
+
values: submittedValues,
|
|
892
|
+
});
|
|
893
|
+
if (url.pathname === "/api/check") {
|
|
894
|
+
const allowedChecks = new Set([
|
|
895
|
+
"telegram",
|
|
896
|
+
"redis",
|
|
897
|
+
"postgres",
|
|
898
|
+
"gateway",
|
|
899
|
+
"rabbitmq",
|
|
900
|
+
]);
|
|
901
|
+
const check = typeof body.check === "string" &&
|
|
902
|
+
allowedChecks.has(body.check)
|
|
903
|
+
? body.check
|
|
904
|
+
: null;
|
|
905
|
+
if (!check) {
|
|
906
|
+
writeJson(response, 400, {
|
|
907
|
+
message: "Неизвестный тип проверки.",
|
|
908
|
+
});
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
writeJson(response, 200, await runConfigureConnectionCheck({ kind: check, role, values }));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const validation = validateConfiguredEnvironment({
|
|
915
|
+
role,
|
|
916
|
+
values,
|
|
917
|
+
});
|
|
918
|
+
if (url.pathname === "/api/validate") {
|
|
919
|
+
writeJson(response, 200, validation);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (!validation.valid) {
|
|
923
|
+
writeJson(response, 422, {
|
|
924
|
+
message: "Исправьте ошибки конфигурации перед скачиванием.",
|
|
925
|
+
errors: validation.errors,
|
|
926
|
+
});
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const content = renderConfiguredEnvironment({
|
|
930
|
+
role,
|
|
931
|
+
template: input.templates[role],
|
|
932
|
+
values,
|
|
933
|
+
});
|
|
934
|
+
response.writeHead(200, {
|
|
935
|
+
"content-type": "text/plain; charset=utf-8",
|
|
936
|
+
"content-disposition": `attachment; filename="${roleDefinition.filename}"`,
|
|
937
|
+
"cache-control": "no-store",
|
|
938
|
+
"x-content-type-options": "nosniff",
|
|
939
|
+
});
|
|
940
|
+
response.end(content);
|
|
941
|
+
input.onDownloaded?.(roleDefinition.filename);
|
|
942
|
+
setTimeout(() => server.close(() => resolve()), 1_000).unref();
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
writeJson(response, 404, { message: "Not found." });
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
writeJson(response, 500, {
|
|
949
|
+
message: error instanceof Error ? error.message : String(error),
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
server.once("error", reject);
|
|
954
|
+
server.listen(input.port ?? 0, "127.0.0.1", () => {
|
|
955
|
+
const address = server.address();
|
|
956
|
+
if (!address || typeof address === "string") {
|
|
957
|
+
reject(new Error("Could not determine configurator address."));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const url = `http://127.0.0.1:${address.port}/?token=${encodeURIComponent(token)}`;
|
|
961
|
+
input.onListening?.(url);
|
|
962
|
+
if (input.open !== false)
|
|
963
|
+
openBrowser(url);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
}
|