@deadragdoll/tellymcp 0.0.9 → 0.0.10

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.
Files changed (27) hide show
  1. package/.env.example.client +2 -0
  2. package/.env.example.gateway +2 -0
  3. package/VERSION.md +26 -0
  4. package/config/templates/env.both.template +1 -0
  5. package/config/templates/env.client.template +1 -0
  6. package/config/templates/env.gateway.template +1 -0
  7. package/dist/services/features/telegram-mcp/approval.service.js +1 -1
  8. package/dist/services/features/telegram-mcp/browser.service.js +1 -1
  9. package/dist/services/features/telegram-mcp/collaboration.service.js +1 -1
  10. package/dist/services/features/telegram-mcp/gateway-rmq.service.js +3 -2
  11. package/dist/services/features/telegram-mcp/gateway-socket.service.js +8 -2
  12. package/dist/services/features/telegram-mcp/inbox.service.js +1 -1
  13. package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -1
  14. package/dist/services/features/telegram-mcp/notify.service.js +1 -1
  15. package/dist/services/features/telegram-mcp/pair.service.js +1 -1
  16. package/dist/services/features/telegram-mcp/runtime.service.js +27 -7
  17. package/dist/services/features/telegram-mcp/session-context.service.js +1 -1
  18. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +2 -1
  19. package/dist/services/features/telegram-mcp/src/app/config/env.js +4 -0
  20. package/dist/services/features/telegram-mcp/src/shared/i18n/index.js +46 -0
  21. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +555 -0
  22. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +555 -0
  23. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +9 -0
  24. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +1148 -575
  25. package/dist/services/features/telegram-mcp/standalone-http.service.js +1 -1
  26. package/dist/services/features/telegram-mcp/tools-sync.service.js +1 -1
  27. package/package.json +2 -1
@@ -0,0 +1,555 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ruMenu = exports.ruCommon = void 0;
4
+ exports.ruCommon = {
5
+ errors: {
6
+ no_telegram_identity: "Для этого чата недоступна Telegram-идентичность.",
7
+ no_active_session: "Активная сессия ещё не привязана. Привяжи её через /start <code>.",
8
+ missing_telegram_context: "Не удалось определить Telegram-пользователя или чат.",
9
+ },
10
+ menu: {
11
+ refreshed: "Меню обновлено.",
12
+ gateway_unavailable: "Gateway недоступен",
13
+ no_telegram_identity_label: "Нет Telegram identity",
14
+ no_active_session_label: "Нет активной сессии",
15
+ back: "⬅ Назад",
16
+ refresh: "🔄 Обновить",
17
+ close: "✖ Закрыть",
18
+ delete: "🗑 Удалить",
19
+ get: "📥 Получить",
20
+ },
21
+ };
22
+ exports.ruMenu = {
23
+ main: {
24
+ buttons: {
25
+ live: "🖥 Live",
26
+ content: "📄 Content",
27
+ browser: "🌐 Browser",
28
+ local: "🏠 Local",
29
+ collab: "👥 Collab",
30
+ storage: "📦 Storage",
31
+ settings: "⚙ Settings",
32
+ back: "⬅ Назад",
33
+ },
34
+ actions: {
35
+ open_content: "Открываю меню content.",
36
+ open_browser: "Открываю browser.",
37
+ open_inbox: "Открываю inbox.",
38
+ open_storage: "Открываю storage.",
39
+ open_settings: "Открываю настройки.",
40
+ back_to_sessions: "Назад к сессиям.",
41
+ },
42
+ screen: {
43
+ title: "🎛 Сессия: {{sessionName}}",
44
+ inbox_messages: "📥 Сообщений в inbox: {{count}}",
45
+ project: "📦 Проект: <b>{{projectName}}</b>",
46
+ partner: "🤝 Напарник: <b><i>{{partnerName}}</i></b>",
47
+ partner_hint: "Делись с напарником деталями API, изменениями, ошибками и git-контекстом.",
48
+ link_hint: "🔗 Свяжи напарника, чтобы координироваться через общие заметки и файлы.",
49
+ tmux_mode_direct: "🖧 Режим TMUX: direct",
50
+ },
51
+ },
52
+ sessions: {
53
+ screen: {
54
+ title: "🗂 Выбери активную сессию",
55
+ last_worked: "🕘 Последняя работа: <i>{{sessionName}}</i>",
56
+ updated: "⏱ Обновлено: <i>{{timestamp}}</i>",
57
+ current_active: "📌 Текущая активная: <b>{{sessionName}}</b>",
58
+ no_linked_sessions: "Для этой Telegram identity не найдено привязанных сессий.",
59
+ },
60
+ labels: {
61
+ no_linked_sessions: "🫥 Нет привязанных сессий",
62
+ unavailable: "⚠ Сессии недоступны",
63
+ tools: "🛠 Tools",
64
+ },
65
+ actions: {
66
+ no_linked_sessions: "Для этой Telegram identity не найдено привязанных сессий.",
67
+ unavailable: "Меню сессий временно недоступно.",
68
+ refreshed: "Список сессий обновлён.",
69
+ open_tools: "Открываю инструменты.",
70
+ },
71
+ },
72
+ inbox: {
73
+ button: "📥 Inbox",
74
+ button_count: "📥 Inbox ({{count}})",
75
+ screen: {
76
+ title: "📥 Inbox",
77
+ active_session: "📌 Активная сессия: {{sessionName}}",
78
+ stored_messages: "📨 Сохранённых сообщений: {{count}}",
79
+ choose_message: "Выбери сообщение ниже, чтобы просмотреть или удалить его.",
80
+ empty: "Для этой сессии нет сохранённых внеочередных Telegram-сообщений.",
81
+ },
82
+ labels: {
83
+ empty: "📭 Inbox пуст",
84
+ },
85
+ actions: {
86
+ empty: "Нет сохранённых внеочередных Telegram-сообщений.",
87
+ refreshed: "Inbox обновлён.",
88
+ },
89
+ },
90
+ buffer: {
91
+ buttons: {
92
+ visible: "👁 Видимое",
93
+ full: "🧾 Полное",
94
+ last_300: "📄 Последние 300",
95
+ last_1000: "📄 Последние 1000",
96
+ },
97
+ screen: {
98
+ title: "📄 Content",
99
+ active_session: "📌 Активная сессия: {{sessionName}}",
100
+ tmux_target: "🖥 tmux target: {{tmuxTarget}}",
101
+ export_hint: "Выбери, сколько истории pane экспортировать в Markdown-файл.",
102
+ export_modes: "Visible отдаёт текущий viewport pane. Full экспортирует всю доступную tmux-историю.",
103
+ },
104
+ },
105
+ browser: {
106
+ buttons: {
107
+ screenshots: "📸 Скриншоты",
108
+ screenshots_count: "📸 Скриншоты ({{count}})",
109
+ },
110
+ actions: {
111
+ open_screenshots: "Открываю скриншоты.",
112
+ back_to_session_menu: "Назад к меню сессии.",
113
+ refreshed: "Скриншоты обновлены.",
114
+ back_to_browser_menu: "Назад к browser.",
115
+ },
116
+ screen: {
117
+ title: "🌐 Browser",
118
+ active_session: "📌 Активная сессия: {{sessionName}}",
119
+ stored_screenshots: "📸 Сохранённых скриншотов: {{count}}",
120
+ choose_action: "Выбери действие, связанное с browser, ниже.",
121
+ },
122
+ },
123
+ screenshots: {
124
+ screen: {
125
+ title: "📸 Скриншоты",
126
+ active_session: "📌 Активная сессия: {{sessionName}}",
127
+ stored_screenshots: "📦 Сохранённых скриншотов: {{count}}",
128
+ choose_screenshot: "Выбери скриншот ниже, чтобы получить его в Telegram или удалить.",
129
+ empty: "Для этой сессии нет сохранённых browser-скриншотов.",
130
+ },
131
+ labels: {
132
+ empty: "📭 Нет скриншотов",
133
+ },
134
+ actions: {
135
+ empty: "Для этой сессии нет сохранённых скриншотов.",
136
+ refreshed: "Скриншоты обновлены.",
137
+ back_to_screenshots: "Назад к скриншотам.",
138
+ },
139
+ },
140
+ storage: {
141
+ buttons: {
142
+ get: "📥 Получить",
143
+ delete: "🗑 Удалить",
144
+ },
145
+ actions: {
146
+ back_to_storage: "Назад к storage.",
147
+ refreshed: "Storage обновлён.",
148
+ empty: "В .mcp-xchange для этой сессии пока нет файлов.",
149
+ },
150
+ screen: {
151
+ title: "📦 Storage",
152
+ active_session: "📌 Активная сессия: {{sessionName}}",
153
+ stored_files: "📦 Сохранённых файлов: {{count}}",
154
+ choose_file: "Выбери файл ниже, чтобы просмотреть его или отправить в Telegram.",
155
+ empty: "Для этой сессии нет сохранённых .mcp-xchange файлов.",
156
+ },
157
+ labels: {
158
+ empty: "📭 Storage пуст",
159
+ },
160
+ },
161
+ settings: {
162
+ buttons: {
163
+ info: "ℹ Info",
164
+ rename: "✏ Rename",
165
+ unpair: "🗑 Unpair",
166
+ confirm_unpair: "⚠ Подтвердить отвязку",
167
+ },
168
+ actions: {
169
+ confirm_unpair: "Подтверди отвязку.",
170
+ back_to_settings: "Назад к настройкам.",
171
+ rename_prompt: "Отправь новое название сессии.",
172
+ rename_body: "Отправь следующим текстовым сообщением новое название активной сессии.\nКоманды вроде /menu или /help отменят режим переименования.",
173
+ },
174
+ screen: {
175
+ title: "⚙ Settings",
176
+ active_session: "📌 Активная сессия: {{sessionName}}",
177
+ hint: "Открой информацию о сессии, переименуй её или отвяжи от Telegram.",
178
+ },
179
+ },
180
+ local: {
181
+ buttons: {
182
+ partner: "🤝 Напарник",
183
+ link: "🔗 Связать",
184
+ unlink: "🔓 Разорвать",
185
+ },
186
+ actions: {
187
+ open_local: "Открываю локальное взаимодействие.",
188
+ back_to_session_menu: "Назад к меню сессии.",
189
+ },
190
+ screen: {
191
+ title: "🏠 Local",
192
+ active_session: "📌 Активная сессия: {{sessionName}}",
193
+ link_status: "🤝 Связь: {{linkedSessionName}}",
194
+ link_status_none: "🤝 Связь: не настроена",
195
+ hint_title: "Здесь живёт локальная работа в одном боте:",
196
+ hint_body: "связка сессий, обмен note и файлами без gateway.",
197
+ unavailable: "Локальное взаимодействие недоступно для этого чата.",
198
+ no_active_session: "Активная сессия не выбрана. Сначала привяжи её через /start.",
199
+ },
200
+ },
201
+ link: {
202
+ buttons: {
203
+ link: "🔗 Связать",
204
+ unlink_with_name: "🔓 Разорвать {{sessionName}}",
205
+ unlink: "🔓 Разорвать",
206
+ },
207
+ labels: {
208
+ no_partner_sessions: "🫥 Нет сессий напарника",
209
+ },
210
+ actions: {
211
+ no_partner_sessions: "Нет других привязанных сессий.",
212
+ back_to_session_menu: "Назад к меню сессии.",
213
+ unlinked: "Связь с напарником снята.",
214
+ choose_partner: "Выбери сессию напарника.",
215
+ },
216
+ screen: {
217
+ title: "🔗 Связать напарника",
218
+ active_session: "📌 Активная сессия: {{sessionName}}",
219
+ choose_partner: "Выбери другую сессию, чтобы связать её как напарника.",
220
+ hint: "Используй эту связку, чтобы делиться API summary, изменениями, ошибками и важным git-контекстом через .mcp-xchange заметки и файлы.",
221
+ },
222
+ },
223
+ partner: {
224
+ buttons: {
225
+ ask: "❓ Ask",
226
+ share: "📤 Share",
227
+ unlink: "🔓 Unlink",
228
+ },
229
+ actions: {
230
+ back_to_session_menu: "Назад к меню сессии.",
231
+ open_partner_menu: "Открываю меню напарника.",
232
+ back_to_partner: "Назад к напарнику.",
233
+ cancel_note_input: "Отправка note напарнику отменена.",
234
+ no_pending_note_input: "Нет активного ввода для note напарнику.",
235
+ task_sent: "Задача отправлена выбранной сессии.",
236
+ inbox_queued: "Задача поставлена в inbox текущей сессии.",
237
+ },
238
+ screen: {
239
+ title: "🤝 Напарник",
240
+ active_session: "📌 Активная сессия: {{sessionName}}",
241
+ linked_partner: "👥 Связанный напарник: {{partnerName}}",
242
+ no_partner: "Напарник пока не связан.",
243
+ use_link_first: "Сначала используй Link в меню сессии.",
244
+ prompt_hint: "Задай вопрос по API или поделись изменениями.",
245
+ prompt_format: "Формат prompt: первая строка — summary. При необходимости добавь пустую строку и основной текст ниже.",
246
+ default_partner: "напарник",
247
+ executor: "Исполнитель: {{label}}",
248
+ route_result: "Маршрут результата: {{source}} -> {{target}}",
249
+ route_send: "Маршрут отправки: {{source}} -> {{target}}",
250
+ type: "Тип: {{kind}}",
251
+ summary: "Кратко: {{summary}}",
252
+ status: "Статус: {{status}}",
253
+ delivered: "доставлено",
254
+ queued: "в очереди",
255
+ current_session_handles: "Текущая сессия подготовит результат и отправит его сама.",
256
+ },
257
+ },
258
+ collab: {
259
+ buttons: {
260
+ tools: "🛠 Tools",
261
+ create: "➕ Создать",
262
+ join: "🔑 Войти",
263
+ broadcast: "📣 Broadcast",
264
+ history: "🕘 History",
265
+ delete: "🗑 Delete",
266
+ },
267
+ actions: {
268
+ gateway_only: "Проекты доступны только через gateway.",
269
+ open_tools: "Открываю инструменты проекта.",
270
+ open_collab: "Открываю Collab.",
271
+ open_delete: "Открываю удаление проектов.",
272
+ back_to_collab: "Назад к Collab.",
273
+ back_to_tools: "Назад к инструментам.",
274
+ back_to_session_menu: "Назад к меню сессии.",
275
+ no_projects: "Проектов пока нет. Создай или войди в существующий.",
276
+ },
277
+ labels: {
278
+ no_projects: "🫥 Нет проектов",
279
+ },
280
+ screen: {
281
+ title: "👥 Collab",
282
+ gateway_not_configured: "Gateway не настроен для этого запуска.",
283
+ use_local_instead: "Для локальной работы в одном боте используй раздел Local.",
284
+ unavailable: "Collab недоступен для текущей сессии.",
285
+ active_session: "📌 Активная сессия: {{sessionName}}",
286
+ open_project: "📦 Открытый проект: {{projectName}}",
287
+ open_project_none: "📦 Открытый проект: не выбран",
288
+ project_count: "🗂 Доступно проектов: {{count}}",
289
+ invite_hint: "Открой проект, создай новый или войди по invite-коду.",
290
+ tools_title: "🛠 Collab Tools",
291
+ tools_empty: "Сначала создай проект или войди в существующий.",
292
+ tools_project_count: "🗂 Проектов в Collab: {{count}}",
293
+ tools_session_count: "👥 Уникальных сессий: {{count}}",
294
+ tools_broadcast: "Broadcast отправит следующее текстовое сообщение всем уникальным Collab-сессиям на ботах без дублирования.",
295
+ tools_history: "History отправит .md с последними 5 Collab-событиями текущей сессии.",
296
+ },
297
+ },
298
+ help: {
299
+ title: "❓ Справка TellyMCP",
300
+ menu: "/menu - открыть список сессий",
301
+ help: "/help - показать эту справку",
302
+ how_it_works: "Как это работает:",
303
+ step_choose: "- выбери активную сессию",
304
+ step_inbox: "- обычные Telegram-сообщения попадают в inbox этой сессии",
305
+ step_nudge: "- если настроен tmux target, сервис автоматически будит агента",
306
+ step_tools: "- затем агент читает batch inbox через MCP tools",
307
+ },
308
+ live: {
309
+ buttons: {
310
+ fullscreen: "Fullscreen",
311
+ expand: "Expand",
312
+ default: "Default",
313
+ },
314
+ actions: {
315
+ opening: "Открываю Live View.",
316
+ choose_mode: "Выбери режим открытия:",
317
+ opened_info: "Live view открыт.",
318
+ approval_unavailable: "Эта сессия сейчас недоступна для подтверждения.",
319
+ },
320
+ errors: {
321
+ identity_unavailable: "Telegram identity недоступна.",
322
+ no_active_session: "Активная сессия не выбрана.",
323
+ webapp_disabled: "WebApp не включён на сервере.",
324
+ public_url_missing: "Публичный URL WebApp не настроен.",
325
+ },
326
+ screen: {
327
+ launcher_title: "🖥 Live: {{sessionName}}",
328
+ },
329
+ approval: {
330
+ request_title: "🖥 Запрос Live view",
331
+ request_message: "Сессия {{sourceSessionName}} запрашивает доступ к Live view вашей сессии.",
332
+ route: "Сессия: {{sourceSessionName}} -> {{targetSessionName}}",
333
+ project: "Проект: {{projectName}}",
334
+ approve: "Разрешить",
335
+ deny: "Отклонить",
336
+ approved: "✅ Доступ к Live разрешён.",
337
+ denied: "❌ Доступ к Live отклонён.",
338
+ source_open: "Открыть Live",
339
+ result_approved: "Live view разрешён для {{sourceSessionName}} -> {{targetSessionName}}.",
340
+ result_denied: "Live view отклонён для {{sourceSessionName}} -> {{targetSessionName}}.",
341
+ },
342
+ },
343
+ notices: {
344
+ startup: {
345
+ title: "✅ TellyMCP запущен.",
346
+ version: "Версия: {{packageVersion}}",
347
+ protocol: "Протокол: {{protocolVersion}}",
348
+ mode: "Режим: {{mode}}",
349
+ bot: "Бот: @{{botUsername}}",
350
+ sessions: "Привязанных сессий: {{count}}",
351
+ session_list: "Сессии: {{sessions}}",
352
+ mcp: "MCP: {{url}}",
353
+ webapp: "WebApp: {{url}}",
354
+ gateway: "Gateway: {{url}}",
355
+ gateway_ws: "Gateway WS: {{url}}",
356
+ browser: "Browser: {{status}}",
357
+ hint: "Напиши /menu, чтобы открыть меню сессий.",
358
+ },
359
+ project: {
360
+ member_joined: "В проект «{{projectName}}» вошёл участник: {{memberLabel}}.",
361
+ member_left: "Из проекта «{{projectName}}» вышел участник: {{memberLabel}}.",
362
+ deleted: "Проект «{{projectName}}» удалён. Локальные project bindings очищены.",
363
+ new_member: "Новый участник",
364
+ member: "Участник",
365
+ },
366
+ tools: {
367
+ changed: "TOOLS.md обновлён на шлюзе или отсутствует локально.",
368
+ session: "Сессия: {{sessionName}}",
369
+ action_required: "Действие обязательно: вызови refresh_tools_markdown, затем перечитай локальный TOOLS.md и применяй его перед продолжением работы.",
370
+ },
371
+ version: {
372
+ reject: "Шлюз и клиент несовместимы по протоколу. Транспорт этой сессии заблокирован.",
373
+ warn: "Версии шлюза и клиента различаются.",
374
+ session: "Сессия: {{sessionName}}",
375
+ client: "Клиент: {{packageVersion}} / protocol {{protocolVersion}}",
376
+ gateway: "Шлюз: {{packageVersion}} / protocol {{protocolVersion}}",
377
+ },
378
+ tmux: {
379
+ target_invalid_title: "⚠ Автоматический tmux nudge для сессии {{sessionName}} не сработал.",
380
+ target_invalid_target: "Сохранённый tmux target больше недействителен: {{tmuxTarget}}",
381
+ target_invalid_action: "Перепривяжи tmux target для этой сессии.",
382
+ unavailable_title: "⚠ Автоматический tmux nudge для сессии {{sessionName}} пропущен.",
383
+ unavailable_body: "tmux сейчас недоступен на этой машине.",
384
+ unavailable_target: "tmux target: {{tmuxTarget}}",
385
+ unavailable_reason: "Обычно это значит, что tmux session/server не запущен или недоступен по текущему socket path.",
386
+ unavailable_action: "Запусти tmux и агента внутри него, либо обнови/сними tmux target для этой сессии.",
387
+ },
388
+ },
389
+ developer: {
390
+ screen: {
391
+ title: "🛠 Tools",
392
+ linked_sessions: "🔗 Привязанных сессий: {{count}}",
393
+ broadcast_help: "Broadcast записывает твоё следующее текстовое сообщение в inbox каждой привязанной сессии и будит все настроенные tmux target.",
394
+ prune_help: "Prune all очищает каждый Redis key в этом namespace telegram-mcp.",
395
+ },
396
+ },
397
+ session_info: {
398
+ opened: "Информация о сессии открыта.",
399
+ title: "ℹ Информация о сессии",
400
+ label: "📌 Label: {{value}}",
401
+ session_id: "🆔 Session ID: {{value}}",
402
+ inbox_count: "📥 Сообщений в inbox: {{count}}",
403
+ paired: "🔗 Привязана: {{value}}",
404
+ partner: "🤝 Напарник: {{value}}",
405
+ tmux_target: "🖥 tmux target: {{value}}",
406
+ tmux_session: "📺 tmux session: {{value}}",
407
+ tmux_window: "🪟 tmux window: {{value}}",
408
+ tmux_pane: "🔹 tmux pane: {{value}}",
409
+ yes: "да",
410
+ no: "нет",
411
+ not_linked: "не связан",
412
+ not_set: "не задан",
413
+ },
414
+ unpair: {
415
+ title: "⚠ Подтверди отвязку",
416
+ active_session: "📌 Активная сессия: {{sessionName}}",
417
+ body_1: "Это удалит Telegram binding для активной сессии.",
418
+ body_2: "Метаданные сессии и записи inbox останутся в Redis, пока ты не удалишь их отдельно.",
419
+ done: "Отвязано: {{sessionName}}",
420
+ shown: "Сессия отвязана: {{sessionName}}",
421
+ },
422
+ prune: {
423
+ title: "⚠ Подтверди prune",
424
+ linked_sessions: "🔗 Видимых здесь привязанных сессий: {{count}}",
425
+ body_1: "Это очистит каждый Redis key внутри namespace telegram-mcp.",
426
+ body_2: "Будут удалены pair codes, bindings, sessions, inbox, menu payloads и pending requests.",
427
+ done: "Prune завершён. Удалено Redis keys: {{count}}.",
428
+ },
429
+ history: {
430
+ title: "# Collab History",
431
+ session: "Сессия: {{sessionName}}",
432
+ empty: "Для этой сессии не найдено недавних Collab-событий.",
433
+ project: "Проект: {{projectName}}",
434
+ caption: "Collab history для {{sessionName}}",
435
+ },
436
+ broadcast: {
437
+ begin: "Broadcast в {{count}} сессий.",
438
+ title: "📣 Broadcast",
439
+ body: "Отправь следующее текстовое сообщение, чтобы разослать его всем {{count}} привязанным сессиям.",
440
+ hint: "Сообщение будет сохранено в inbox каждой сессии, а сервис разбудит все настроенные tmux target.",
441
+ cancel_hint: "Команды вроде /menu или /help отменят режим broadcast.",
442
+ collab_begin: "Broadcast в {{count}} Collab-сессий.",
443
+ collab_title: "📣 Collab Broadcast",
444
+ collab_projects: "Collab проектов: {{count}}",
445
+ collab_sessions: "Уникальных сессий: {{count}}",
446
+ collab_body: "Отправь следующее текстовое сообщение, чтобы разослать его всем Collab-сессиям на ботах без дублирования.",
447
+ collab_hint: "Локальные сессии получат inbox message, удалённые — gateway delivery.",
448
+ no_linked_sessions: "Привязанных сессий не найдено.",
449
+ no_collab_targets: "Нет доступных Collab-сессий для broadcast.",
450
+ no_projects_first: "Сначала создай проект или войди в существующий.",
451
+ mode_not_active: "Режим broadcast не активен.",
452
+ cancelled: "Broadcast отменён.",
453
+ cancelled_no_sessions: "Broadcast отменён, потому что привязанных сессий не найдено.",
454
+ completed_linked: "Broadcast завершён для {{count}} привязанных сессий.",
455
+ completed_collab: "Broadcast завершён для {{count}} Collab-сессий.",
456
+ completed_collab_local: "Локальных inbox: {{count}}",
457
+ completed_collab_remote: "Удалённых deliveries: {{count}}",
458
+ completed_collab_total: "Всего сессий: {{count}}",
459
+ },
460
+ project: {
461
+ not_found: "Проект не найден.",
462
+ left_callback: "Выход из проекта выполнен.",
463
+ left_screen: "Вы вышли из выбранного проекта.",
464
+ deleted_callback: "Проект удалён.",
465
+ deleted_screen: "Проект «{{projectName}}» удалён. Участники отвязаны, project state очищен.",
466
+ delete_only_owner: "Удалять проект может только owner.",
467
+ delete_menu_title: "🗑 Delete Project",
468
+ active_session: "📌 Активная сессия: {{sessionName}}",
469
+ total_count: "🗂 Проектов в Collab: {{count}}",
470
+ owner_count: "👑 Проектов, где ты owner: {{count}}",
471
+ delete_choose: "Выбери проект для полного удаления.",
472
+ delete_body: "Будут отвязаны участники, удалены project sessions и сама запись проекта.",
473
+ delete_owner_hint: "Удалять проект может только owner.",
474
+ created: "Проект создан: {{projectName}}\nInvite: {{inviteToken}}",
475
+ joined: "Вход в проект выполнен: {{projectName}}",
476
+ opened: "Открыт проект: {{projectName}}",
477
+ invalid_action: "Некорректное действие проекта.",
478
+ invalid_member_action: "Некорректное действие для участника проекта.",
479
+ invalid_member_payload: "Некорректные данные участника проекта.",
480
+ stale_member_payload: "Данные участника проекта некорректны или устарели.",
481
+ invalid_live_payload: "Некорректные данные Live View.",
482
+ stale_live_payload: "Данные Live View некорректны или устарели.",
483
+ no_telegram_user: "Не удалось определить Telegram пользователя.",
484
+ request_live_sent: "Запрос на Live отправлен на подтверждение.",
485
+ loading_members: "Загружаю участников.",
486
+ opening_members: "Открываю участников проекта.",
487
+ opening_session: "Открываю сессию.",
488
+ opening_files: "Открываю файлы.",
489
+ create_prompt_title: "📦 Создать проект",
490
+ create_prompt_body: "Отправь следующим сообщением имя проекта.",
491
+ join_prompt_title: "🔑 Вступить в проект",
492
+ join_prompt_body: "Отправь следующим сообщением invite token проекта.",
493
+ prompt_cancel: "Команды вроде /menu или /help отменят этот режим.",
494
+ start_create: "Создание проекта.",
495
+ start_join: "Вход в проект.",
496
+ data_missing: "Данные проекта не найдены.",
497
+ data_stale: "Данные проекта устарели или некорректны.",
498
+ no_active_project: "Нет активного проекта.",
499
+ left_current: "Выход из проекта выполнен.",
500
+ left_current_screen: "Вы вышли из текущего проекта.",
501
+ members_title: "👥 Участники {{projectName}}",
502
+ current_session: "Сессия: {{sessionName}}",
503
+ other_sessions: "Других сессий: {{count}}",
504
+ choose_file_target: "Выбери, кому передать этот файл.",
505
+ choose_member_action: "Выбери сессию, чтобы спросить, поделиться, ответить или передать.",
506
+ no_other_active: "В этом проекте пока нет других активных сессий.",
507
+ leave: "🚪 Выйти",
508
+ back_to_projects: "⬅ К проектам",
509
+ ask: "❓ Спросить",
510
+ share_button: "📤 Поделиться",
511
+ back_to_members: "⬅ К участникам",
512
+ back_to_session: "⬅ К сессии",
513
+ file_title: "📎 Выбор файла",
514
+ file_project: "Проект: {{projectName}}",
515
+ file_recipient: "Получатель: {{label}}",
516
+ file_choose: "Выбери файл для отправки.",
517
+ file_none: "В этой сессии нет загруженных файлов.",
518
+ invalid_approval: "Некорректное подтверждение Live View.",
519
+ invalid_approval_data: "Некорректные данные подтверждения.",
520
+ approval_stale: "Запрос Live View устарел.",
521
+ },
522
+ handoff: {
523
+ no_pending: "Нет ожидающего prompt передачи файла.",
524
+ cancelled: "Передача файла отменена.",
525
+ delivered_agent: "Файл передан агенту.",
526
+ queued_partner: "Файл поставлен в очередь доставки напарнику.",
527
+ project: "Проект: {{projectName}}",
528
+ recipient: "Получатель: {{label}}",
529
+ session: "Сессия: {{label}}",
530
+ status: "Статус: {{status}}",
531
+ share: "Share: {{shareId}}",
532
+ delivered: "доставлено",
533
+ queued: "в очереди",
534
+ prompt_title: "🤝 Передать участнику",
535
+ route: "Сессия: {{sourceSessionName}} -> {{targetSessionName}}",
536
+ file: "Файл: {{fileName}}",
537
+ prompt_body: "Отправь следующим сообщением описание или инструкции для этого файла.",
538
+ prompt_hint: "Этот текст будет приложен к handoff.",
539
+ choose_title: "📎 Выбор файла",
540
+ choose_recipient: "Получатель: {{label}}",
541
+ choose_local: "Выбери файл для отправки локальному напарнику.",
542
+ choose_project: "Выбери файл для отправки.",
543
+ no_files: "В этой сессии нет загруженных файлов.",
544
+ cancel: "Отмена",
545
+ uploaded_to_session: "Файл отправлен в сессию {{label}}.",
546
+ uploaded_to_partner: "Файл отправлен напарнику {{label}}.",
547
+ delivered_one: "Файл доставлен в сессию {{label}}.",
548
+ delivered_many: "Файлы доставлены в сессию {{label}}: {{count}}.",
549
+ },
550
+ system: {
551
+ sessions_menu_unavailable: "Меню сессий временно недоступно. Попробуй /menu ещё раз.",
552
+ error_prefix: "Ошибка: {{message}}",
553
+ tmux_recreated_hint: "Обычно это значит, что pane/window/session был пересоздан.",
554
+ },
555
+ };
@@ -35,6 +35,9 @@ function inboxMessageKey(sessionId, messageId) {
35
35
  function menuPayloadKey(key) {
36
36
  return `${KEY_PREFIX}:menu-payload:${key}`;
37
37
  }
38
+ function userLocaleKey(telegramUserId) {
39
+ return `${KEY_PREFIX}:user-locale:${telegramUserId}`;
40
+ }
38
41
  function gatewayClientUuidKey() {
39
42
  return `${KEY_PREFIX}:gateway:client-uuid`;
40
43
  }
@@ -113,6 +116,12 @@ class RedisStateStore {
113
116
  const raw = await this.redis.get(bindingKey(sessionId));
114
117
  return parseJson(raw);
115
118
  }
119
+ async getUserLocale(telegramUserId) {
120
+ return (await this.redis.get(userLocaleKey(telegramUserId))) ?? null;
121
+ }
122
+ async setUserLocale(telegramUserId, locale) {
123
+ await this.redis.set(userLocaleKey(telegramUserId), locale);
124
+ }
116
125
  async setBinding(binding) {
117
126
  const previous = await this.getBinding(binding.sessionId);
118
127
  if (previous &&