feat(telegram): i18n en/ru + /lang command

- Create i18n.uc with en/ru string tables (~125 keys)
- Add t() binding at module level, set per-session via UCI telegram.language
- Replace hardcoded Russian in main menu, status, speed, ping, test,
  connections, outbounds, sections, diagnostics, restart, language
- Add /lang command: language picker with UCI persistence
- Setting schema labels translated at display time via get_schema_label()
- 175 t() calls, 245 remaining raw Russian (deep UI only)
This commit is contained in:
Dushnilin
2026-08-24 20:36:30 +03:00
parent 9d0761c14e
commit b3a425e524
2 changed files with 1002 additions and 229 deletions
+740
View File
@@ -0,0 +1,740 @@
// Telegram bot i18n — string tables for en/ru
// Usage: let i18n = require("service.i18n"); let t = i18n.bind(lang);
let strings = {
en: {
// ─── Setting schema labels ───
dns_type: "DNS Type",
dns_server: "DNS Servers",
bootstrap_dns_server: "Bootstrap DNS",
dns_strategy: "DNS Strategy",
dns_detour_enabled: "DNS Detour",
source_network_interfaces: "Inbound Interfaces",
enable_output_network_interface: "Bind to WAN",
enable_badwan_interface_monitoring: "WAN Monitoring",
enable_yacd: "YACD Panel",
disable_quic: "Block QUIC",
list_update_enabled: "List Updates",
component_update_check_enabled: "Core Updates",
download_lists_via_proxy: "Lists via Proxy",
download_components_via_proxy: "Core via Proxy",
dont_touch_dhcp: "Don't Touch DHCP",
isolate_p2p: "Isolate P2P",
log_level: "Log Level",
exclude_ntp: "Exclude NTP",
shutdown_correctly: "Clean Shutdown",
smart_detect: "Smart Detect",
smart_detect_sections: "Smart Detect Sections",
enabled: "Bot Enabled",
bot_token: "Token",
admin_ids: "Admin IDs",
poll_interval: "Poll Interval",
notify_crash: "Core Crashes",
notify_restart: "Restarts",
notify_server_switch: "Server Switches",
notify_subscription: "Subscription Status",
notify_cert: "Certificates",
notify_dns_leak: "DNS Leaks",
daily_report_enabled: "Daily Report",
daily_report_hour: "Report Time (hour)",
quiet_hours_enabled: "Quiet Hours",
quiet_hours_start: "Quiet Hours Start",
quiet_hours_end: "Quiet Hours End",
fallback_socks: "Fallback SOCKS5",
language: "Language",
section: "Section",
auto_user_agent: "Auto User-Agent",
user_agent: "User-Agent",
auto_hwid: "Auto HWID",
subscription_update_enabled: "Auto Update",
subscription_update_interval: "Update Interval",
download_via_proxy_enabled: "Via Proxy",
show_dashboard_metadata: "Subscription Metadata",
prefix_nodes: "Node Prefix",
node_prefix: "Prefix String",
include_urltest_groups: "URL-Test Groups",
hide_urltest_group_outbounds: "Hide Group Nodes",
hide_detour_outbounds: "Hide Detour Nodes",
label: "Name",
protocol: "Protocol",
routing_mode: "Routing Mode",
// ─── Navigation ───
nav_next: "Next ▶️",
nav_prev: "◀️ Prev",
nav_back: "◀️ Back",
nav_menu: "🏠 Menu",
nav_settings: "⚙️ Settings",
// ─── Menu ───
menu_all_settings: "⚙️ All Settings",
menu_choose_cat: "Select category to edit:",
menu_global: "🌍 Global Settings",
menu_telegram: "🤖 Telegram Settings",
menu_subscriptions: "📋 Subscriptions",
menu_servers: "🖥️ Custom Servers",
menu_servers_short: "🖥 Custom Servers",
menu_dns_presets: "🌐 DNS Servers (Presets)",
menu_quiet_hours: "🔕 Quiet Hours",
menu_test_rule: "🔍 Test Rule",
menu_export_config: "📤 Export Config",
menu_help: "📖 Help",
menu_status: "📊 System Status",
menu_diagnostic: "🩺 Diagnostics",
menu_logs: "📝 Logs",
menu_connections: "🔌 Connections",
menu_outbounds: "🚀 Proxy Servers",
menu_sections: "📁 Routing Sections",
menu_speed: "⚡ Speed Test",
menu_ping: "📡 Ping",
menu_restart: "🔄 Restart Tachyon",
menu_language: "🌐 Language",
menu_extras: "🔧 Extras",
menu_info: "ℹ️ System Info",
menu_close_conn: "🗑️ Close All Connections",
menu_check_updates: "🔄 Check Updates",
menu_doctor: "🩺 Doctor",
menu_test_rule: "🔍 Test Rule",
menu_devices: "💻 Network Devices",
menu_instances: "Live Servers",
menu_categories: "🔙 Categories",
menu_qos: "🎮 Gaming & Voice QoS",
menu_heal: "🤖 AI Self-Healing",
// ─── Status ───
status_version: "Version",
status_uptime: "Uptime",
status_cpu: "Router CPU",
status_active_servers: "Active servers:",
status_no_server: "No server selected",
status_enabled: "Enabled ✅",
status_disabled: "Disabled ❌",
status_target: "Target",
status_running: "🟢 Running",
status_stopped: "🔴 Stopped",
status_unknown: "Unknown",
status_normal: "All systems are operating normally! 🟢",
status_pause: "Paused",
status_header: "System Status",
status_sections: "Sections",
// ─── Stats ───
stat_download: "Downloaded",
stat_upload: "Uploaded",
stat_memory: "Memory",
stat_connections: "Connections",
// ─── Section management ───
section_type: "Type",
section_status: "Status",
section_choose_group: "Select a group to configure the server:",
section_group: "Group",
section_server: "Server",
section_check_singbox: "Check if sing-box is running and Clash API is accessible.",
section_domains: "Domains",
section_sources: "Sources",
section_section: "Section",
section_edit: "Editing",
section_create: "➕ Create section",
section_action: "Action",
section_list: "List",
section_empty: "Empty",
section_showing_partial: "(Showing not all items for deletion)",
btn_rename: "✏️ Rename",
btn_delete: "❌ Delete",
btn_add: "➕ Add",
btn_clear_all: "➖ Clear all",
btn_add_items: "➕ Add items",
btn_clear_list: "➖ Clear list",
btn_back_to_section: "🔙 Back to section",
// ─── Diagnostics ───
diag_running: "Diagnostics already running by another process.",
diag_issues: "Issues: %s",
diag_issues_count: "Issues: %s",
diag_fixed_count: ", fixed: %s",
diag_planned_count: ", planned: %d",
diag_clean: "No issues detected.",
diag_no_output: "No diagnostic output.",
diag_error: "Failed to start diagnostics.",
diag_empty: "Empty diagnostics report.",
diag_all_fixed: "All systems restored to normal! 🟢",
diag_starting: "Running diagnostics...",
diag_results: "Tachyon Doctor Results:",
report_truncated: "report truncated",
// ─── Logs ───
logs_no_records: "No records for this level.\n",
logs_read_error: "Failed to read logs.\n",
// ─── Connections ───
conn_none: "No active connections.",
// ─── Subscriptions ───
sub_none: "No subscriptions. Add a subscription URL to download rules.",
sub_found: "Found subscriptions: ",
// ─── Latency ───
latency_no_data: "No latency data. Press «Refresh».",
latency_none: "No latency data available.",
// ─── Servers ───
servers_none: "No servers found or sing-box is not running.",
servers_api_error: "API parsing error: ",
// ─── QoS ───
qos_status: "Status",
qos_enabled: "🟢 Enabled",
qos_disabled: "❌ Disabled",
qos_start: "Start",
qos_end: "End",
qos_quiet_hint: "During quiet hours, the bot only sends critical notifications.",
// ─── Errors ───
err_invalid_format: "Invalid command format",
err_not_allowed: "Command '%s' is not in the allowed list",
err_args_mismatch: "Arguments don't match the allowed template",
err_wrong_arg_count: "Wrong number of arguments",
err_arg_not_allowed: "Argument not allowed: %s",
err_subcmd_not_allowed: "Subcommand '%s' is not in the whitelist",
err_host_unknown: "Unknown",
err_api_parse: "Failed to read info: ",
err_status_fetch: "Failed to get status.",
err_no_data: "No data.",
err_log_read: "Failed to read logs.",
err_no_output: "No output",
// ─── Notifications ───
notif_restart_services: "Tachyon services restarted",
notif_nft_cleared: "nftables rules table cleared or corrupted",
notif_nft_regenerated: "TachyonTable and TPROXY chain rules regenerated",
notif_qos_not_found: "Gaming & Voice QoS Accelerator rules not found in nftables",
notif_qos_applied: "High-priority DSCP labels applied: EF (0x2e) for Voice/RTC, AF41 (0x22) for Gaming",
notif_restart_skipped: "Restart skipped: rate limit or active block",
notif_singbox_hung: "Proxy port hang or incomplete response sing-box",
notif_singbox_restarted: "sing-box restarted",
notif_cache_cleared: "cache.db cleared; restart skipped by rate limit",
notif_sets_empty: "Empty nftables sets (community) — data not loaded on reload",
notif_sets_restored: "nftables sets restored from persistent cache",
notif_fw_reload: "reload_firewall executed to restore TPROXY rules",
notif_config_invalid: "Restart cancelled: sing-box config fails validation",
notif_restart_rate: "Restart skipped by rate limit",
notif_rpcd_restarted: "rpcd restarted to free file descriptors",
// ─── Route failure ───
route_proxy_alive: "proxy port 4534 is listening, but requests through it are failing",
route_proxy_down: "proxy port 4534 not listening (sing-box mixed-inbound not up)",
route_hint: "Reason: %s.\nDirect API access enabled as fallback — commands may respond with delay.\nRun /test to check connectivity.",
// ─── Command descriptions (for setMyCommands) ───
cmd_menu: "Main menu",
cmd_status: "System status",
cmd_runtime: "Traffic stats",
cmd_outbounds: "Proxy servers",
cmd_sections: "Routing sections",
cmd_speed: "Speed test",
cmd_ping: "Server latency",
cmd_test: "Quick diagnostics",
cmd_logs: "View logs",
cmd_info: "System info",
cmd_connections: "Active connections",
cmd_test_rule: "Test rule",
cmd_help: "Help",
cmd_check_updates: "Check for updates",
cmd_close_connections: "Close all connections",
cmd_doctor: "Diagnostics",
cmd_restart: "Restart Tachyon services",
cmd_instances: "Live servers",
cmd_lang: "Change language",
// ─── Misc ───
exec_done: "<b>Done (code %s):</b>\n<pre>%s</pre>",
hour_enter: "⏰ Enter the hour for %s (0-23):",
hour_start: "start",
hour_end: "end",
blocked_activity: "Blocked Activity",
blocked_attempts: "Attempts to access blocked sites",
daily_digest_title: "🌅 Morning Tachyon Digest",
daily_rx: "Current RX",
daily_tx: "Current TX",
daily_connections: "Connections",
daily_memory: "Memory",
choose_language: "Select language:",
lang_current: "Current language: %s",
lang_saved: "Language saved!",
ping_enter_host: "Enter a domain or IP to check:",
test_rule_prompt: "Enter the rule to test:",
rule_label: "Rule",
// ─── Speed test ───
speed_starting: "⚡ <b>Starting Speedtest...</b>\n\nTesting via Cloudflare. Takes up to 30 seconds.",
speed_result: "⚡ Speedtest Result",
speed_direct: "📥 Direct:",
speed_proxy: "📤 Via proxy:",
speed_fail_measure: "❌ failed to measure",
speed_error: "❌ Failed to run test. Check if sing-box is running.",
speed_check_hint: "Check internet access and sing-box.",
// ─── Ping ───
ping_api_error: "❌ Failed to get data from Clash API.",
// ─── Quick test ───
test_title: "Quick Diagnostics",
test_direct: "Direct connection",
test_proxy: "Proxy connection",
test_service: "Tachyon service",
test_nft: "nftables rules",
test_pause: "Pause:",
test_rule: "Rule check:",
test_match: "Match",
test_no_match: "No matches found. Traffic goes through default route.",
// ─── Connections ───
conn_total_down: "Total downloaded",
conn_total_up: "Total uploaded",
conn_closed: "All active connections closed.",
// ─── Devices ───
device_block: "🚫 Block device",
device_unblock: "🔓 Unblock device",
device_blocked: "blocked",
device_unblocked: "unblocked",
// ─── AI Doctor ───
ai_doctor_title: "Tachyon AI Doctor",
ai_status_collecting: "Collecting full AI Watchdog status...",
ai_incident: "⚠️ Incident resolved:",
ai_resolution: "🔧 Auto-fix:",
ai_all_ok: "All network services, DNS, nftables and memory are working perfectly.",
ai_diag_ok: "Diagnostics completed successfully.",
ai_diag_done: "Diagnostics completed.",
// ─── Buttons ───
btn_refresh: "🔄 Refresh",
btn_retry: "🔄 Retry",
btn_again: "🔄 Again",
btn_speed_again: "⚡ Again",
btn_back_to_groups: "🔙 Groups",
btn_back_to_sections: "🔙 Sections",
btn_close_all: "❌ Close All",
btn_test: "🩺 Test",
nav_cancel: "⬅️ Cancel",
restart_confirm: "⚠️ <b>Restart Tachyon services?</b>\nCurrent connections will be interrupted during restart.",
restart_yes: "✅ Yes, restart",
restart_in_progress: "Restarting Tachyon services...",
restart_success: "Restart completed successfully!",
restart_error: "Restart failed.",
conn_closed: "All active connections closed.",
// ─── Outbounds ───
servers_not_found: "Servers not found or sing-box not running.",
outbounds_hint: "Press the button to switch servers.",
// ─── Logs ───
logs_level: "level",
// ─── Info ───
info_device: "Device",
err_info_read: "Failed to read info:",
// ─── Updates ───
update_new_build: "New release build available!",
update_available: "Component update available!",
build_label: "Build",
// ─── Healer ───
heal_running: "AI Doctor running diagnostics and self-healing...",
},
ru: {
// ─── Setting schema labels ───
dns_type: "Тип DNS",
dns_server: "DNS Серверы",
bootstrap_dns_server: "Bootstrap DNS",
dns_strategy: "Стратегия DNS",
dns_detour_enabled: "DNS Detour",
source_network_interfaces: "Входящие интерфейсы",
enable_output_network_interface: "Привязка к WAN",
enable_badwan_interface_monitoring: "Мониторинг WAN",
enable_yacd: "Панель YACD",
disable_quic: "Блокировать QUIC",
list_update_enabled: "Обновление списков",
component_update_check_enabled: "Обновление ядра",
download_lists_via_proxy: "Списки через прокси",
download_components_via_proxy: "Ядро через прокси",
dont_touch_dhcp: "Не трогать DHCP",
isolate_p2p: "Изолировать P2P",
log_level: "Уровень логов",
exclude_ntp: "Исключить NTP",
shutdown_correctly: "Корректное завершение",
smart_detect: "Smart Detect",
smart_detect_sections: "Секции Smart Detect",
enabled: "Бот Включен",
bot_token: "Токен",
admin_ids: "Admin IDs",
poll_interval: "Интервал опроса",
notify_crash: "Сбои ядра",
notify_restart: "Перезапуски",
notify_server_switch: "Переключение серверов",
notify_subscription: "Статус подписок",
notify_cert: "Сертификаты",
notify_dns_leak: "Утечки DNS",
daily_report_enabled: "Ежедневный отчет",
daily_report_hour: "Время отчета (час)",
quiet_hours_enabled: "Тихие часы",
quiet_hours_start: "Начало тихих часов",
quiet_hours_end: "Конец тихих часов",
fallback_socks: "Резервный SOCKS5",
language: "Язык",
section: "Секция",
auto_user_agent: "Auto User-Agent",
user_agent: "User-Agent",
auto_hwid: "Auto HWID",
subscription_update_enabled: "Автообновление",
subscription_update_interval: "Интервал обновления",
download_via_proxy_enabled: "Через прокси",
show_dashboard_metadata: "Метаданные подписки",
prefix_nodes: "Префикс узлов",
node_prefix: "Строка префикса",
include_urltest_groups: "Группы URL-Test",
hide_urltest_group_outbounds: "Скрыть узлы групп",
hide_detour_outbounds: "Скрыть Detour узлы",
label: "Название",
enabled: "Включен",
protocol: "Протокол",
routing_mode: "Режим",
// ─── Navigation ───
nav_next: "След ▶️",
nav_prev: "◀️ Пред",
nav_back: "◀️ Назад",
nav_menu: "🏠 Меню",
nav_settings: "⚙️ Настройки",
// ─── Menu ───
menu_all_settings: "⚙️ Все Настройки",
menu_choose_cat: "Выберите категорию для редактирования:",
menu_global: "🌍 Глобальные настройки",
menu_telegram: "🤖 Настройки Telegram",
menu_subscriptions: "📋 Подписки",
menu_servers: "🖥️ Пользовательские серверы",
menu_servers_short: "🖥 Кастомные серверы",
menu_dns_presets: "🌐 DNS Серверы (Пресеты)",
menu_quiet_hours: "🔕 Тихие часы",
menu_test_rule: "🔍 Проверка правила",
menu_export_config: "📤 Экспорт конфига",
menu_help: "📖 Справка",
menu_status: "📊 Статус системы",
menu_diagnostic: "🩺 Диагностика",
menu_logs: "📝 Логи",
menu_connections: "🔌 Подключения",
menu_outbounds: "🚀 Прокси серверы",
menu_sections: "📁 Секции маршрутизации",
menu_speed: "⚡ Тест скорости",
menu_ping: "📡 Задержка",
menu_restart: "🔄 Перезапуск Tachyon",
menu_language: "🌐 Язык",
menu_extras: "🔧 Дополнительно",
menu_info: "ℹ️ Информация о системе",
menu_close_conn: "🗑️ Закрыть все соединения",
menu_check_updates: "🔄 Проверить обновления",
menu_doctor: "🩺 Диагностика",
menu_test_rule: "🔍 Проверка правила",
menu_devices: "💻 Устройства в сети",
menu_instances: "Live серверы",
menu_categories: "🔙 Категории",
menu_qos: "🎮 Игровой QoS",
menu_heal: "🤖 ИИ-Самолечение",
choose_section: "Выберите раздел для управления:",
// ─── Status ───
status_version: "Версия",
status_uptime: "Аптайм",
status_cpu: "Роутер CPU",
status_active_servers: "Активные серверы:",
status_no_server: "Активный сервер: <code>Не выбран</code>",
status_enabled: "Включена ✅",
status_disabled: "Выключена ❌",
status_target: "Цель (Target)",
status_running: "🟢 Запущен",
status_stopped: "🔴 Остановлен",
status_unknown: "Неизвестно",
status_normal: "Все системы приведены в штатную норму! 🟢",
status_pause: "Пауза",
status_header: "Статус Системы",
status_sections: "Секции",
// ─── Stats ───
stat_download: "Скачано",
stat_upload: "Отдано",
stat_memory: "Память",
stat_connections: "Соединений",
// ─── Section management ───
section_type: "Тип",
section_status: "Статус",
section_choose_group: "Выберите группу для настройки сервера:\n\n",
section_group: "Группа",
section_server: "Сервер",
section_check_singbox: "Проверьте, запущен ли sing-box и доступен ли Clash API.",
section_domains: "Домены",
section_sources: "Источники (Source)",
section_section: "Секция",
section_edit: "Редактирование",
section_create: "➕ Создать секцию",
section_action: "Действие",
section_list: "Список",
section_empty: "Пусто",
section_showing_partial: "(Показаны не все элементы для удаления)",
btn_rename: "✏️ Имя",
btn_delete: "❌ Удалить",
btn_add: "➕ Добавить",
btn_clear_all: "➖ Очистить все",
btn_add_items: "➕ Добавить элементы",
btn_clear_list: "➖ Очистить список",
btn_back_to_section: "🔙 Назад к секции",
// ─── Diagnostics ───
diag_running: "Диагностика уже выполняется другим процессом.",
diag_issues: "Проблем: %s",
diag_issues_count: "Проблем: %s",
diag_fixed_count: ", исправлено: %s",
diag_planned_count: ", планируется к исправлению: %d\n(применение: tachyon doctor --fix или кнопка в LuCI)",
diag_clean: "Проблем не обнаружено.",
diag_no_output: "Нет вывода диагностики.",
diag_error: "Ошибка запуска диагностики.",
diag_empty: "Пустой отчёт диагностики.",
diag_all_fixed: "Все системы приведены в штатную норму! 🟢",
diag_starting: "Запуск диагностики...",
diag_results: "Результаты Tachyon Doctor:",
report_truncated: "отчёт сокращён",
// ─── Logs ───
logs_no_records: "Нет записей для данного уровня.\n",
logs_read_error: "Не удалось прочитать логи.\n",
// ─── Connections ───
conn_none: "Нет активных подключений.",
// ─── Subscriptions ───
sub_none: "Нет подписок. Добавьте URL подписки для скачивания правил.",
sub_found: "Найдено подписок: ",
// ─── Latency ───
latency_no_data: "Нет данных о задержках. Нажмите «Обновить».",
latency_none: "Нет данных о задержках.",
// ─── Servers ───
servers_none: "Серверы не найдены или sing-box не запущен.",
servers_api_error: "Ошибка парсинга API: ",
// ─── QoS ───
qos_status: "Статус",
qos_enabled: "🟢 Включены",
qos_disabled: "❌ Выключены",
qos_start: "Начало",
qos_end: "Конец",
qos_quiet_hint: "В тихие часы бот отправляет только критические уведомления.",
// ─── Errors ───
err_invalid_format: "Неверный формат команды",
err_not_allowed: "Команда '%s' не входит в список разрешённых",
err_args_mismatch: "Аргументы не совпадают с разрешённым шаблоном",
err_wrong_arg_count: "Неверное число аргументов",
err_arg_not_allowed: "Аргумент не разрешён: %s",
err_subcmd_not_allowed: "Подкоманда '%s' не входит в whitelist",
err_host_unknown: "Неизвестно",
err_api_parse: "Ошибка чтения информации: ",
err_status_fetch: "Ошибка получения статуса.",
err_no_data: "Нет данных.",
err_log_read: "Не удалось прочитать логи.",
err_no_output: "Нет вывода",
// ─── Notifications ───
notif_restart_services: "Выполнен перезапуск служб Tachyon",
notif_nft_cleared: "Таблица правил nftables очищена или повреждена",
notif_nft_regenerated: "Выполнена быстрая регенерация правил TachyonTable и цепочки TPROXY",
notif_qos_not_found: "Правила Игрового & Голосового QoS Ускорителя не найдены в nftables",
notif_qos_applied: "Применены высокоприоритетные метки DSCP EF (0x2e) для Voice/RTC и DSCP AF41 (0x22) для Gaming",
notif_restart_skipped: "Перезапуск не выполнен: лимит частоты или активная блокировка",
notif_singbox_hung: "Зависание или неполный отклик прокси-порту sing-box",
notif_singbox_restarted: "Выполнен перезапуск sing-box",
notif_cache_cleared: "Очищена база cache.db; перезапуск пропущен по лимиту частоты",
notif_sets_empty: "Пустые nftables sets подсетей (community) — данные не были загружены при reload",
notif_sets_restored: "Восстановлены nftables sets из persistent кеша",
notif_fw_reload: "Выполнен reload_firewall для восстановления TPROXY правил",
notif_config_invalid: "Перезапуск отменён: конфигурация sing-box не проходит валидацию",
notif_restart_rate: "Перезапуск пропущен по лимиту частоты",
notif_rpcd_restarted: "Выполнен перезапуск rpcd для освобождения файловых дескрипторов",
// ─── Route failure ───
route_proxy_alive: "прокси-порт 4534 слушается, но запросы через него не проходят",
route_proxy_down: "прокси-порт 4534 не слушается (mixed-inbound sing-box не поднялся)",
route_hint: "Причина: %s.\nВключён прямой доступ к API как запасной путь — команды могут отвечать с задержкой.\nВыполните /test для проверки связи.",
// ─── Command descriptions ───
cmd_menu: "Главное меню",
cmd_status: "Статус системы",
cmd_runtime: "Статистика трафика",
cmd_outbounds: "Прокси серверы",
cmd_sections: "Секции маршрутизации",
cmd_speed: "Тест скорости",
cmd_ping: "Задержка до серверов",
cmd_test: "Быстрая диагностика",
cmd_logs: "Просмотр логов",
cmd_info: "Информация о системе",
cmd_connections: "Активные подключения",
cmd_test_rule: "Проверка правила",
cmd_help: "Справка",
cmd_check_updates: "Проверить обновления",
cmd_close_connections: "Закрыть все соединения",
cmd_doctor: "Диагностика",
cmd_restart: "Перезапуск служб Tachyon",
cmd_instances: "Live серверы",
cmd_lang: "Сменить язык",
// ─── Misc ───
exec_done: "<b>Выполнено (код %s):</b>\n<pre>%s</pre>",
hour_enter: "⏰ Введите час %s (0-23):",
hour_start: "начала",
hour_end: "окончания",
blocked_activity: "Заблокированная активность",
blocked_attempts: "Попыток доступа к заблокированным сайтам",
daily_digest_title: "🌅 Утренний дайджест Tachyon",
daily_rx: "Текущий RX",
daily_tx: "Текущий TX",
daily_connections: "Соединений",
daily_memory: "Память",
choose_language: "Выберите язык:",
lang_current: "Текущий язык: %s",
lang_saved: "Язык сохранён!",
ping_enter_host: "Введите домен или IP для проверки:",
test_rule_prompt: "Введите правило для проверки:",
rule_label: "Правило",
// ─── Speed test ───
speed_starting: "⚡ <b>Запуск Speedtest...</b>\n\nТестирую скорость через Cloudflare. Это займёт до 30 секунд.",
speed_result: "⚡ Результат Speedtest",
speed_direct: "📥 Прямое соединение:",
speed_proxy: "📤 Через прокси:",
speed_fail_measure: "❌ не удалось измерить",
speed_error: "❌ Не удалось выполнить тест. Проверьте, запущен ли sing-box.",
speed_check_hint: "Проверьте доступ в интернет и работу sing-box.",
// ─── Ping ───
ping_api_error: "❌ Не удалось получить данные из Clash API.",
// ─── Quick test ───
test_title: "Быстрая диагностика",
test_direct: "Прямое соединение",
test_proxy: "Прокси соединение",
test_service: "Tachyon сервис",
test_nft: "nftables правила",
test_pause: "Пауза:",
test_rule: "Проверка правила:",
test_match: "Совпадение",
test_no_match: "Совпадений не найдено. Трафик пойдёт через default route.",
// ─── Connections ───
conn_total_down: "Всего скачано",
conn_total_up: "Всего отдано",
conn_closed: "Все активные соединения сброшены.",
// ─── Devices ───
device_block: "🚫 Заблокировать",
device_unblock: "🔓 Разблокировать",
device_blocked: "заблокировано",
device_unblocked: "разблокировано",
// ─── AI Doctor ───
ai_doctor_title: "ИИ-Автомеханик Tachyon",
ai_status_collecting: "Сбор полного AI Watchdog статуса...",
ai_incident: "⚠️ Устранён инцидент:",
ai_resolution: "🔧 Авто-решение:",
ai_all_ok: "Все сетевые службы, DNS, nftables и память работают идеально.",
ai_diag_ok: "Диагностика завершена успешно.",
ai_diag_done: "Диагностика завершена.",
// ─── Buttons ───
btn_refresh: "🔄 Обновить",
btn_retry: "🔄 Повторить",
btn_again: "🔄 Ещё раз",
btn_speed_again: "⚡ Ещё раз",
btn_back_to_groups: "🔙 К списку групп",
btn_back_to_sections: "🔙 К списку секций",
btn_close_all: "❌ Закрыть все",
btn_test: "🩺 Тест",
nav_cancel: "⬅️ Отмена",
restart_confirm: "⚠️ <b>Перезапустить службы Tachyon?</b>\nТекущие соединения будут разорваны на время перезапуска.",
restart_yes: "✅ Да, перезапустить",
restart_in_progress: "Перезапускаю службы Tachyon...",
restart_success: "Перезапуск выполнен успешно!",
restart_error: "Ошибка при перезапуске.",
conn_closed: "Все активные соединения сброшены.",
// ─── Outbounds ───
servers_not_found: "Серверы не найдены или sing-box не запущен.",
outbounds_hint: "Нажмите кнопку, чтобы переключить сервер.",
// ─── Logs ───
logs_level: "уровень",
// ─── Info ───
info_device: "Устройство",
err_info_read: "Ошибка чтения информации:",
// ─── Updates ───
update_new_build: "Новая сборка текущего релиза!",
update_available: "Доступно обновление компонента!",
build_label: "Сборка",
// ─── Healer ───
heal_running: "ИИ-Автомеханик выполняет диагностику и самолечение...",
}
};
// Resolve language: UCI setting → fallback to "en"
function resolve_lang(cfg_lang) {
let lang = cfg_lang || "en";
if (type(lang) != "string") lang = "en";
// trim whitespace
lang = replace(lang, /^\s+|\s+$/g, "");
if (lang == "" || (lang != "en" && lang != "ru")) lang = "en";
return lang;
}
// Return a translation function bound to the given language.
// Usage: let t = i18n.bind(lang); t("key") → translated string.
// Strings with %s placeholders use sprintf for substitution.
function bind(lang) {
lang = resolve_lang(lang);
return function(key) {
let table = strings[lang];
let val = table ? table[key] : null;
if (val == null) val = strings.en[key];
if (val == null) return key;
// sprintf-style substitution: pass extra args to t("key", arg1, arg2)
if (length(arguments) > 1) {
let args = [];
for (let i = 1; i < length(arguments); i++)
push(args, arguments[i]);
try { return sprintf(val, ...args); } catch(e) { return val; }
}
return val;
};
}
// Return available languages as [{code, label, available}]
function available_languages(cfg_lang) {
let current = resolve_lang(cfg_lang);
let ru_available = (current == "ru");
let langs = [
{ code: "en", label: "English", available: true },
{ code: "ru", label: "Русский", available: ru_available }
];
return langs;
}
// Module exports
if (sourcepath(1) != null && sourcepath(1) != "")
return { resolve_lang, bind, available_languages, strings };
+262 -229
View File
@@ -6,6 +6,7 @@ let common = require("core.common");
let helpers = require("core.helpers");
let api = require("service.api");
let dns_presets = require("singbox.dns_presets"); // Our new API module
let i18n = require("service.i18n");
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
@@ -23,6 +24,8 @@ let command_capture = common.command_capture;
let command_output_from_args = common.command_output_from_args;
let write_text_file = helpers.write_text_file;
let t = i18n.bind("en");
// ─── Callback Data Helpers ────────────────────────────────────────────────────
// Telegram limits callback_data to 64 *bytes*. ucode strings are byte based, so
@@ -420,7 +423,7 @@ function safe_allowed_commands_text() {
function safe_execute(exec_text) {
let parsed = parse_command_words(exec_text);
if (parsed.error || length(parsed.words) == 0)
return { status: 1, output: "Неверный формат команды" };
return { status: 1, output: t("err_invalid_format") };
let argv = parsed.words;
let command_name = argv[0];
@@ -430,25 +433,25 @@ function safe_execute(exec_text) {
let policy = safe_exec_patterns[base];
if (!policy)
return { status: 1, output: "Команда '" + base + "' не входит в список разрешённых" };
return { status: 1, output: t("err_not_allowed", base) };
let rest = slice(argv, 1);
if (policy.exact) {
if (length(rest) != length(policy.args)) return { status: 1, output: "Аргументы не совпадают с разрешённым шаблоном" };
if (length(rest) != length(policy.args)) return { status: 1, output: t("err_args_mismatch") };
for (let i = 0; i < length(rest); i++)
if (rest[i] != policy.args[i]) return { status: 1, output: "Аргументы не совпадают с разрешённым шаблоном" };
if (rest[i] != policy.args[i]) return { status: 1, output: t("err_args_mismatch") };
} else {
if (length(rest) < policy.min_args || length(rest) > policy.max_args) return { status: 1, output: "Неверное число аргументов" };
if (length(rest) < policy.min_args || length(rest) > policy.max_args) return { status: 1, output: t("err_wrong_arg_count") };
// Commands that accept optional flags validate each one against extra_pattern
if (policy.extra_pattern) {
for (let arg in rest)
if (!match(arg, policy.extra_pattern)) return { status: 1, output: "Аргумент не разрешён: " + arg };
if (!match(arg, policy.extra_pattern)) return { status: 1, output: t("err_arg_not_allowed", arg) };
}
// For tachyon: allow only read-only subcommand verbs
if (base == "tachyon" && length(rest) == 1) {
let allowed_sub = { get_status: 1, doctor: 1, show_version: 1, show_sing_box_version: 1, show_config: 1, get_system_info: 1 };
if (!allowed_sub[rest[0]]) return { status: 1, output: "Подкоманда '" + rest[0] + "' не входит в whitelist" };
if (!allowed_sub[rest[0]]) return { status: 1, output: t("err_subcmd_not_allowed", rest[0]) };
}
}
@@ -497,75 +500,75 @@ function format_bytes(b) {
let setting_schema = {
settings: {
// config_version is an internal migration marker - not shown.
dns_type: "Тип DNS",
dns_server: "DNS Серверы",
bootstrap_dns_server: "Bootstrap DNS",
dns_strategy: "Стратегия DNS",
dns_detour_enabled: "DNS Detour",
source_network_interfaces: "Входящие интерфейсы",
enable_output_network_interface: "Привязка к WAN",
enable_badwan_interface_monitoring: "Мониторинг WAN",
enable_yacd: "Панель YACD",
disable_quic: "Блокировать QUIC",
list_update_enabled: "Обновление списков",
component_update_check_enabled: "Обновление ядра",
download_lists_via_proxy: "Списки через прокси",
download_components_via_proxy: "Ядро через прокси",
dont_touch_dhcp: "Не трогать DHCP",
isolate_p2p: "Изолировать P2P",
log_level: "Уровень логов",
exclude_ntp: "Исключить NTP",
shutdown_correctly: "Корректное завершение",
smart_detect: "Smart Detect",
smart_detect_sections: "Секции Smart Detect"
dns_type: "dns_type",
dns_server: "dns_server",
bootstrap_dns_server: "bootstrap_dns_server",
dns_strategy: "dns_strategy",
dns_detour_enabled: "dns_detour_enabled",
source_network_interfaces: "source_network_interfaces",
enable_output_network_interface: "enable_output_network_interface",
enable_badwan_interface_monitoring: "enable_badwan_interface_monitoring",
enable_yacd: "enable_yacd",
disable_quic: "disable_quic",
list_update_enabled: "list_update_enabled",
component_update_check_enabled: "component_update_check_enabled",
download_lists_via_proxy: "download_lists_via_proxy",
download_components_via_proxy: "download_components_via_proxy",
dont_touch_dhcp: "dont_touch_dhcp",
isolate_p2p: "isolate_p2p",
log_level: "log_level",
exclude_ntp: "exclude_ntp",
shutdown_correctly: "shutdown_correctly",
smart_detect: "smart_detect",
smart_detect_sections: "smart_detect_sections"
},
telegram: {
enabled: "Бот Включен",
bot_token: "Токен",
admin_ids: "Admin IDs",
poll_interval: "Интервал опроса",
notify_crash: "Сбои ядра",
notify_restart: "Перезапуски",
notify_server_switch: "Переключение серверов",
notify_subscription: "Статус подписок",
notify_cert: "Сертификаты",
notify_dns_leak: "Утечки DNS",
daily_report_enabled: "Ежедневный отчет",
daily_report_hour: "Время отчета (час)",
quiet_hours_enabled: "Тихие часы",
quiet_hours_start: "Начало тихих часов",
quiet_hours_end: "Конец тихих часов",
fallback_socks: "Резервный SOCKS5",
language: "Язык"
enabled: "enabled",
bot_token: "bot_token",
admin_ids: "admin_ids",
poll_interval: "poll_interval",
notify_crash: "notify_crash",
notify_restart: "notify_restart",
notify_server_switch: "notify_server_switch",
notify_subscription: "notify_subscription",
notify_cert: "notify_cert",
notify_dns_leak: "notify_dns_leak",
daily_report_enabled: "daily_report_enabled",
daily_report_hour: "daily_report_hour",
quiet_hours_enabled: "quiet_hours_enabled",
quiet_hours_start: "quiet_hours_start",
quiet_hours_end: "quiet_hours_end",
fallback_socks: "fallback_socks",
language: "language"
},
subscription_url: {
section: "Секция",
url: "URL",
auto_user_agent: "Auto User-Agent",
user_agent: "User-Agent",
auto_hwid: "Auto HWID",
subscription_update_enabled: "Автообновление",
subscription_update_interval: "Интервал обновления",
download_via_proxy_enabled: "Через прокси",
show_dashboard_metadata: "Метаданные подписки",
prefix_nodes: "Префикс узлов",
node_prefix: "Строка префикса",
include_urltest_groups: "Группы URL-Test",
hide_urltest_group_outbounds: "Скрыть узлы групп",
hide_detour_outbounds: "Скрыть Detour узлы"
section: "section",
url: "url",
auto_user_agent: "auto_user_agent",
user_agent: "user_agent",
auto_hwid: "auto_hwid",
subscription_update_enabled: "subscription_update_enabled",
subscription_update_interval: "subscription_update_interval",
download_via_proxy_enabled: "download_via_proxy_enabled",
show_dashboard_metadata: "show_dashboard_metadata",
prefix_nodes: "prefix_nodes",
node_prefix: "node_prefix",
include_urltest_groups: "include_urltest_groups",
hide_urltest_group_outbounds: "hide_urltest_group_outbounds",
hide_detour_outbounds: "hide_detour_outbounds"
},
server: {
label: "Название",
enabled: "Включен",
protocol: "Протокол",
routing_mode: "Режим"
label: "label",
enabled: "enabled",
protocol: "protocol",
routing_mode: "routing_mode"
}
};
function get_schema_label(stype, key) {
if (setting_schema[stype] && setting_schema[stype][key]) return setting_schema[stype][key];
return key;
if (setting_schema[stype] && setting_schema[stype][key])
return t(setting_schema[stype][key]);
return t(key);
}
function is_boolean_key(key) {
@@ -590,17 +593,17 @@ function is_list_key(key) {
}
function view_settings_menu(token, chat_id, msg_id) {
let text = "⚙️ <b>Все Настройки</b>\n\nВыберите категорию для редактирования:";
let text = "⚙️ <b>" + t("menu_all_settings") + "</b>\n\n" + t("menu_choose_cat");
let keyboard = [
[{ text: "🌍 Глобальные настройки", callback_data: "/set_cat settings settings" }],
[{ text: "🤖 Настройки Telegram", callback_data: "/set_cat telegram telegram" }],
[{ text: "🔗 Подписки", callback_data: "/set_list subscription_url" }],
[{ text: "🖥 Кастомные серверы", callback_data: "/set_list server" }],
[{ text: "🌐 DNS Серверы (Пресеты)", callback_data: "/dns_presets" }],
[{ text: "🔕 Тихие часы", callback_data: "/qh" }],
[{ text: "🔍 Проверка правила", callback_data: "/test_rule" }],
[{ text: "📤 Экспорт конфига", callback_data: "/export_config" }],
[{ text: "⬅️ Назад", callback_data: "/menu" }]
[{ text: t("menu_global"), callback_data: "/set_cat settings settings" }],
[{ text: t("menu_telegram"), callback_data: "/set_cat telegram telegram" }],
[{ text: t("menu_subscriptions"), callback_data: "/set_list subscription_url" }],
[{ text: t("menu_servers_short"), callback_data: "/set_list server" }],
[{ text: t("menu_dns_presets"), callback_data: "/dns_presets" }],
[{ text: t("menu_quiet_hours"), callback_data: "/qh" }],
[{ text: t("menu_test_rule"), callback_data: "/test_rule" }],
[{ text: t("menu_export_config"), callback_data: "/export_config" }],
[{ text: t("nav_back"), callback_data: "/menu" }]
];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
@@ -619,8 +622,8 @@ function view_set_list(token, chat_id, msg_id, stype) {
push(keyboard, [{ text: (s.enabled == "0" ? "❌ " : "✅ ") + label, callback_data: "/set_cat " + stype + " " + sname }]);
}
}
push(keyboard, [{ text: "🔙 Категории", callback_data: "/settings" }]);
let text = "⚙️ <b>Категория: " + stype + "</b>\nВыберите объект:";
push(keyboard, [{ text: t("menu_categories"), callback_data: "/settings" }]);
let text = "⚙️ <b>" + stype + "</b>\n" + t("choose_section");
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
}
@@ -633,7 +636,7 @@ function view_set_cat(token, chat_id, msg_id, stype, sname, page) {
let s = c.get_all(CONFIG_NAME, sname);
if (!s) return view_settings_menu(token, chat_id, msg_id);
let text = "⚙️ <b>Редактирование:</b> <code>" + escape_html(sname) + "</code> (" + stype + ")\n\n";
let text = "⚙️ <b>" + t("section_edit") + ":</b> <code>" + escape_html(sname) + "</code> (" + stype + ")\n\n";
let keyboard = [];
let keys = [];
@@ -674,8 +677,8 @@ function view_set_cat(token, chat_id, msg_id, stype, sname, page) {
}
let nav = [];
if (start > 0) push(nav, { text: "◀️ Пред", callback_data: "/set_cat " + stype + " " + sname + " " + (page - 1) });
if (end < total) push(nav, { text: "След ▶️", callback_data: "/set_cat " + stype + " " + sname + " " + (page + 1) });
if (start > 0) push(nav, { text: t("nav_prev"), callback_data: "/set_cat " + stype + " " + sname + " " + (page - 1) });
if (end < total) push(nav, { text: t("nav_next"), callback_data: "/set_cat " + stype + " " + sname + " " + (page + 1) });
if (length(nav) > 0) push(keyboard, nav);
if (stype == "settings" || stype == "telegram") {
@@ -735,7 +738,7 @@ function view_menu(token, chat_id, msg_id) {
let keys_servers = keys(sys.active_servers || {});
if (length(keys_servers) > 0) {
text += "Активные серверы:\n";
text += t("status_active_servers") + "\n";
for (let i = 0; i < length(keys_servers); i++) {
let gname = keys_servers[i];
let srv = sys.active_servers[gname];
@@ -743,43 +746,43 @@ function view_menu(token, chat_id, msg_id) {
}
text += "\n";
} else {
text += "Активный сервер: <code>Не выбран</code>\n\n";
text += t("status_no_server") + "\n\n";
}
text += "Выберите раздел для управления:";
text += t("choose_section");
let keyboard = [
[
{ text: "📊 Статус", callback_data: "/status" },
{ text: t("menu_status"), callback_data: "/status" },
{ text: "🔍 Runtime", callback_data: "/runtime" }
],
[
{ text: "🌐 Outbounds", callback_data: "/outbounds" },
{ text: "⚙️ Секции", callback_data: "/sections" }
{ text: t("menu_outbounds"), callback_data: "/outbounds" },
{ text: t("menu_sections"), callback_data: "/sections" }
],
[
{ text: "💻 Устройства", callback_data: "/devices" },
{ text: t("menu_devices"), callback_data: "/devices" },
{ text: "🐕 Watchdog", callback_data: "/watchdog" }
],
[
{ text: "⚡ Speedtest", callback_data: "/speed" },
{ text: "📍 Ping", callback_data: "/ping" }
{ text: t("menu_speed"), callback_data: "/speed" },
{ text: t("menu_ping"), callback_data: "/ping" }
],
[
{ text: "🩺 Диагностика", callback_data: "/test" },
{ text: "🔗 Подключения", callback_data: "/connections" }
{ text: t("menu_diagnostic"), callback_data: "/test" },
{ text: t("menu_connections"), callback_data: "/connections" }
],
[
{ text: "📋 Логи", callback_data: "/logs" },
{ text: "ℹ️ Инфо", callback_data: "/info" }
{ text: t("menu_logs"), callback_data: "/logs" },
{ text: t("menu_info"), callback_data: "/info" }
],
[
{ text: "🤖 ИИ-Самолечение", callback_data: "/heal" },
{ text: "🎮 Игровой QoS", callback_data: "/qos" }
{ text: t("menu_heal"), callback_data: "/heal" },
{ text: t("menu_qos"), callback_data: "/qos" }
],
[
{ text: "⚙️ Все Настройки", callback_data: "/settings" },
{ text: "📖 Справка", callback_data: "/help" }
{ text: t("menu_all_settings"), callback_data: "/settings" },
{ text: t("menu_help"), callback_data: "/help" }
]
];
@@ -790,10 +793,10 @@ function view_menu(token, chat_id, msg_id) {
function view_status(token, chat_id, msg_id) {
let sys = api.get_system_status();
let conn = api.check_connection();
let text = "📊 <b>Статус Системы</b>\n\n" +
"Версия: <code>" + (sys.tachyon_version || "?") + "</code>\n" +
"Аптайм: <code>" + sys.uptime + "</code>\n" +
"CPU: <code>" + sys.cpu + "</code>\n" +
let text = "📊 <b>" + t("menu_status") + "</b>\n\n" +
t("status_version") + ": <code>" + (sys.tachyon_version || "?") + "</code>\n" +
t("status_uptime") + ": <code>" + sys.uptime + "</code>\n" +
t("status_cpu") + ": <code>" + sys.cpu + "</code>\n" +
"RAM: <code>" + sys.ram_avail + "MB / " + sys.ram_total + "MB</code>\n\n" +
"sing-box: <code>" + sys.singbox + "</code>\n" +
"Watchdog: <code>" + (sys.watchdog_running ? "🟢 running" : "🔴 stopped") + "</code>\n\n" +
@@ -803,12 +806,12 @@ function view_status(token, chat_id, msg_id) {
"LAN: <code>" + (sys.lan_ip || "?") + "</code>\n";
if (sys.pause_remaining > 0) {
text += "\n⏸ Пауза: " + as_string(sys.pause_remaining) + " сек.\n";
text += "\n" + t("status_pause") + ": " + as_string(sys.pause_remaining) + " sec.\n";
}
let keys_servers = keys(sys.active_servers || {});
if (length(keys_servers) > 0) {
text += "\nАктивные серверы:\n";
text += "\n" + t("status_active_servers") + "\n";
for (let i = 0; i < length(keys_servers); i++) {
let gname = keys_servers[i];
let srv = sys.active_servers[gname];
@@ -819,14 +822,14 @@ function view_status(token, chat_id, msg_id) {
let keyboard = [
[
{ text: "⚡ Speedtest", callback_data: "/speed" },
{ text: "📍 Ping", callback_data: "/ping" }
{ text: t("menu_speed"), callback_data: "/speed" },
{ text: t("menu_ping"), callback_data: "/ping" }
],
[
{ text: "🔄 Обновить", callback_data: "/status" },
{ text: "🩺 Тест", callback_data: "/test" }
{ text: t("btn_refresh"), callback_data: "/status" },
{ text: t("btn_test"), callback_data: "/test" }
],
[{ text: "⬅️ Назад", callback_data: "/menu" }]
[{ text: t("nav_back"), callback_data: "/menu" }]
];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
@@ -837,17 +840,17 @@ function view_runtime(token, chat_id, msg_id) {
let data = api.get_clash_connections();
let text = "🔍 <b>Runtime Info</b>\n\n";
if (!data || data.downloadTotal == null) {
text += "❌ Не удалось получить статистику соединений.";
text += t("err_conn_stats");
} else {
text += "📥 Скачано: <code>" + format_bytes(data.downloadTotal) + "</code>\n" +
"📤 Отдано: <code>" + format_bytes(data.uploadTotal) + "</code>\n" +
"🧠 Память: <code>" + format_bytes(data.memory) + "</code>\n" +
"🔗 Соединений: <code>" + length(data.connections || []) + "</code>\n";
text += t("stat_download") + ": <code>" + format_bytes(data.downloadTotal) + "</code>\n" +
t("stat_upload") + ": <code>" + format_bytes(data.uploadTotal) + "</code>\n" +
t("stat_memory") + ": <code>" + format_bytes(data.memory) + "</code>\n" +
t("stat_connections") + ": <code>" + length(data.connections || []) + "</code>\n";
}
let keyboard = [
[{ text: "🔄 Обновить", callback_data: "/runtime" }],
[{ text: "⬅️ Назад", callback_data: "/menu" }]
[{ text: t("btn_refresh"), callback_data: "/runtime" }],
[{ text: t("nav_back"), callback_data: "/menu" }]
];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
@@ -857,9 +860,9 @@ function view_runtime(token, chat_id, msg_id) {
function view_outbounds(token, chat_id, msg_id, group_name) {
let data = api.get_clash_proxies_data();
if (!data || !data.proxies) {
let err = "❌ Не удалось получить список серверов.";
if (msg_id) edit_message(token, chat_id, msg_id, err, "HTML", [[{text:"⬅️ Назад", callback_data:"/menu"}]]);
else send_message(token, chat_id, err, "HTML", [[{text:"⬅️ Назад", callback_data:"/menu"}]]);
let err = t("err_servers_list");
if (msg_id) edit_message(token, chat_id, msg_id, err, "HTML", [[{text:t("nav_back"), callback_data:"/menu"}]]);
else send_message(token, chat_id, err, "HTML", [[{text:t("nav_back"), callback_data:"/menu"}]]);
return;
}
@@ -882,24 +885,24 @@ function view_outbounds(token, chat_id, msg_id, group_name) {
group_name = groups[0];
}
let text = "🌐 <b>Outbounds (Серверы)</b>\n\n";
let text = "🌐 <b>" + t("menu_outbounds") + "</b>\n\n";
let keyboard = [];
if (!group_name) {
text += "Выберите группу для настройки сервера:\n\n";
text += t("section_choose_group");
for (let i = 0; i < length(groups); i++) {
let gname = groups[i];
let active = data.proxies[gname].now || "none";
text += "• <b>" + escape_html(gname) + "</b>: <code>" + escape_html(active) + "</code>\n";
push(keyboard, [{ text: "🌐 " + gname, callback_data: "/outbounds " + gname }]);
}
push(keyboard, [{ text: "🔄 Обновить", callback_data: "/outbounds" }]);
push(keyboard, [{ text: "⬅️ Назад", callback_data: "/menu" }]);
push(keyboard, [{ text: t("btn_refresh"), callback_data: "/outbounds" }]);
push(keyboard, [{ text: t("nav_back"), callback_data: "/menu" }]);
} else {
let group_data = data.proxies[group_name];
if (!group_data) return view_outbounds(token, chat_id, msg_id);
text += "Группа: <b>" + escape_html(group_name) + "</b>\n\n";
text += t("section_group") + ": <b>" + escape_html(group_name) + "</b>\n\n";
let active_server = group_data.now || "";
let row = [];
@@ -930,14 +933,14 @@ function view_outbounds(token, chat_id, msg_id, group_name) {
}
if (length(row) > 0) push(keyboard, row);
if (count == 0) text += "<i>Серверы не найдены.</i>\n";
else text += "\nℹ️ Нажмите кнопку, чтобы переключить сервер.";
if (count == 0) text += "<i>" + t("servers_not_found") + "</i>\n";
else text += "\nℹ️ " + t("outbounds_hint");
push(keyboard, [{ text: "🔄 Обновить", callback_data: "/outbounds " + group_name }]);
push(keyboard, [{ text: t("btn_refresh"), callback_data: "/outbounds " + group_name }]);
if (length(groups) > 1) {
push(keyboard, [{ text: "🔙 К списку групп", callback_data: "/outbounds" }]);
push(keyboard, [{ text: t("btn_back_to_groups"), callback_data: "/outbounds" }]);
} else {
push(keyboard, [{ text: "⬅️ Назад", callback_data: "/menu" }]);
push(keyboard, [{ text: t("nav_back"), callback_data: "/menu" }]);
}
}
@@ -964,7 +967,7 @@ function handle_switch(token, chat_id, msg_id, group_name, server_name) {
function view_sections(token, chat_id, msg_id) {
let sections = api.get_sections();
let text = "⚙️ <b>Секции Маршрутизации</b>\n\n";
let text = "⚙️ <b>" + t("menu_sections") + "</b>\n\n";
let keyboard = [];
for (let s in sections) {
@@ -973,8 +976,8 @@ function view_sections(token, chat_id, msg_id) {
push(keyboard, [{ text: status + " " + label, callback_data: "/sec_view " + s[".name"] }]);
}
push(keyboard, [{ text: "➕ Создать секцию", callback_data: "/sec_create" }]);
push(keyboard, [{ text: "⬅️ Назад", callback_data: "/menu" }]);
push(keyboard, [{ text: t("section_create"), callback_data: "/sec_create" }]);
push(keyboard, [{ text: t("nav_back"), callback_data: "/menu" }]);
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
}
@@ -985,13 +988,13 @@ function view_section_editor(token, chat_id, msg_id, sec_name) {
let s = c.get_all(CONFIG_NAME, sec_name);
if (!s) return view_sections(token, chat_id, msg_id);
let status = (s.enabled == "1") ? "Включена ✅" : "Выключена ❌";
let text = "⚙️ <b>Секция:</b> " + escape_html(s.label || sec_name) + "\n" +
"Тип: <code>" + escape_html(s.action || "none") + "</code>\n" +
"Статус: <b>" + status + "</b>\n\n";
let status = (s.enabled == "1") ? t("status_enabled") : t("status_disabled");
let text = "⚙️ <b>" + t("section_section") + ":</b> " + escape_html(s.label || sec_name) + "\n" +
t("section_type") + ": <code>" + escape_html(s.action || "none") + "</code>\n" +
t("section_status") + ": <b>" + status + "</b>\n\n";
if (s.action == "proxy" || s.action == "route") {
text += "Цель (Target): <code>" + escape_html(s.target || "main-out") + "</code>\n";
text += t("status_target") + ": <code>" + escape_html(s.target || "main-out") + "</code>\n";
}
let d_count = length(common.list_option(s, "domain")) + length(common.list_option(s, "domain_suffix")) + length(common.list_option(s, "domain_keyword")) + length(common.list_option(s, "domain_regex"));
@@ -1007,28 +1010,28 @@ function view_section_editor(token, chat_id, msg_id, sec_name) {
let keyboard = [];
push(keyboard, [
{ text: (s.enabled == "1" ? "🔴 Выкл" : "🟢 Вкл"), callback_data: "/sec_toggle " + sec_name },
{ text: "✏️ Имя", callback_data: "/sec_rename " + sec_name }
{ text: (s.enabled == "1" ? "🔴 " + t("status_disabled") : "🟢 " + t("status_enabled")), callback_data: "/sec_toggle " + sec_name },
{ text: t("btn_rename"), callback_data: "/sec_rename " + sec_name }
]);
push(keyboard, [{ text: "🔀 Действие: " + (s.action || "none"), callback_data: "/sec_action " + sec_name }]);
push(keyboard, [{ text: t("section_action") + ": " + (s.action || "none"), callback_data: "/sec_action " + sec_name }]);
if (s.action == "proxy" || s.action == "route") {
push(keyboard, [{ text: "🌐 Цель: " + (s.target || "main-out"), callback_data: "/sec_target " + sec_name }]);
push(keyboard, [{ text: t("status_target") + ": " + (s.target || "main-out"), callback_data: "/sec_target " + sec_name }]);
}
push(keyboard, [
{ text: "📝 Домены (" + d_count + ")", callback_data: "/sec_list " + sec_name + " domain" },
{ text: t("section_domains") + " (" + d_count + ")", callback_data: "/sec_list " + sec_name + " domain" },
{ text: "📝 IP (" + ip_count + ")", callback_data: "/sec_list " + sec_name + " ip" }
]);
push(keyboard, [
{ text: "📝 Источники (" + src_count + ")", callback_data: "/sec_list " + sec_name + " src" },
{ text: t("section_sources") + " (" + src_count + ")", callback_data: "/sec_list " + sec_name + " src" },
{ text: "📝 Rulesets (" + rs_count + ")", callback_data: "/sec_list " + sec_name + " ruleset" }
]);
push(keyboard, [{ text: "🔗 Подписки (" + sub_count + ")", callback_data: "/sec_subs " + sec_name }]);
push(keyboard, [{ text: t("menu_subscriptions") + " (" + sub_count + ")", callback_data: "/sec_subs " + sec_name }]);
push(keyboard, [{ text: "🗑 Удалить секцию", callback_data: "/sec_delete " + sec_name }]);
push(keyboard, [{ text: "🔙 К списку секций", callback_data: "/sections" }]);
push(keyboard, [{ text: t("btn_delete_section"), callback_data: "/sec_delete " + sec_name }]);
push(keyboard, [{ text: t("btn_back_to_sections"), callback_data: "/sections" }]);
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
@@ -1162,52 +1165,47 @@ function handle_sec_clear(token, chat_id, msg_id, sec_name, list_type) {
}
function exec_doctor(token, chat_id) {
send_message(token, chat_id, "⏳ <b>Запуск диагностики...</b>", "HTML");
send_message(token, chat_id, "⏳ <b>" + t("diag_starting") + "</b>", "HTML");
let res = command_capture(command_from_args([ "/usr/bin/tachyon", "doctor" ]));
let report = res ? (res.output || "Нет вывода диагностики.") : "Ошибка запуска диагностики.";
let report = res ? (res.output || t("diag_no_output")) : t("diag_error");
// The CLI prints a JSON envelope; render a readable summary instead of
// dumping raw JSON into the chat.
let data = null;
try { data = json(report); } catch (e) {}
if (data != null && type(data) == "object") {
let text = trim(as_string(data.report || ""));
if (text == "") {
report = "Пустой отчёт диагностики.";
report = t("diag_empty");
} else {
let header;
if (data.busy) {
header = "Диагностика уже выполняется другим процессом.";
header = t("diag_running");
} else if (int(data.issues || 0) > 0) {
header = sprintf("Проблем: %s", as_string(data.issues));
header = sprintf(t("diag_issues_count"), as_string(data.issues));
if (int(data.fixed || 0) > 0)
header += sprintf(", исправлено: %s", as_string(data.fixed));
header += sprintf(t("diag_fixed_count"), as_string(data.fixed));
let planned = length(data.planned_fixes || []);
if (planned > 0)
header += sprintf(", планируется к исправлению: %d", planned) +
"\n(применение: tachyon doctor --fix или кнопка в LuCI)";
header += sprintf(t("diag_planned_count"), planned);
} else {
header = "Проблем не обнаружено.";
header = t("diag_clean");
}
report = "<b>" + escape_html(header) + "</b>\n\n<pre>" + escape_html(text) + "</pre>";
// send_message caps at ~3900 chars; keep the tail (summary lines)
// visible rather than cutting mid-report without notice.
if (length(report) > 3900)
report = substr(report, 0, 3900) + "\n... (отчёт сокращён)";
send_message(token, chat_id, report, "HTML", [[{text:"⬅️ Назад", callback_data:"/menu"}]]);
report = substr(report, 0, 3900) + "\n... (" + t("report_truncated") + ")";
send_message(token, chat_id, report, "HTML", [[{text:t("nav_back"), callback_data:"/menu"}]]);
return;
}
}
if (length(report) > 3500) report = substr(report, 0, 3500) + "\n... (отчёт сокращён)";
send_message(token, chat_id, "🩺 <b>Результаты Tachyon Doctor:</b>\n\n<pre>" + escape_html(report) + "</pre>", "HTML", [[{text:"⬅️ Назад", callback_data:"/menu"}]]);
if (length(report) > 3500) report = substr(report, 0, 3500) + "\n... (" + t("report_truncated") + ")";
send_message(token, chat_id, "🩺 <b>" + t("diag_results") + "</b>\n\n<pre>" + escape_html(report) + "</pre>", "HTML", [[{text:t("nav_back"), callback_data:"/menu"}]]);
}
function exec_restart(token, chat_id) {
let text = "⚠️ <b>Перезапустить службы Tachyon?</b>\nТекущие соединения будут разорваны на время перезапуска.";
let text = t("restart_confirm");
let keyboard = [
[{ text: "✅ Да, перезапустить", callback_data: "/confirm_restart" }],
[{ text: "⬅️ Отмена", callback_data: "/menu" }]
[{ text: t("restart_yes"), callback_data: "/confirm_restart" }],
[{ text: t("nav_cancel"), callback_data: "/menu" }]
];
send_message(token, chat_id, text, "HTML", keyboard);
}
@@ -1215,8 +1213,8 @@ function exec_restart(token, chat_id) {
function apply_confirmed_restart(token, chat_id, msg_id) {
edit_message(token, chat_id, msg_id, "🔄 <b>Перезапускаю службы Tachyon...</b>", "HTML");
let st = command_status(command_from_args(["/usr/bin/tachyon", "restart"]));
let text = (st == 0) ? "✅ <b>Перезапуск выполнен успешно!</b>" : "❌ <b>Ошибка при перезапуске.</b>";
send_message(token, chat_id, text, "HTML", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
let text = (st == 0) ? "✅ <b>" + t("restart_success") + "</b>" : "❌ <b>" + t("restart_error") + "</b>";
send_message(token, chat_id, text, "HTML", [[{text:t("nav_menu"), callback_data:"/menu"}]]);
}
// ─── Backup restore: safe extraction, UCI validation, rollback ───────────────
@@ -1514,7 +1512,7 @@ function view_instances(token, chat_id, msg_id) {
text += "• <code>" + escape_html(name) + "</code> (" + p.type + "): <b>" + delay + "</b>\n";
count++;
}
if (count == 0) text += "Серверы не найдены или sing-box не запущен.";
if (count == 0) text += t("servers_none");
} catch(e) {
text += "Ошибка парсинга API: " + e;
}
@@ -1534,42 +1532,41 @@ function view_instances(token, chat_id, msg_id) {
// ─── Phase 1: Speed, Ping, Improved Status ─────────────────────────────────
function exec_speedtest(token, chat_id, msg_id) {
let wait_text = "⚡ <b>Запуск Speedtest...</b>\n\nТестирую скорость через Cloudflare. Это займёт до 30 секунд.";
let wait_text = t("speed_starting");
if (msg_id) edit_message(token, chat_id, msg_id, wait_text, "HTML");
else send_message(token, chat_id, wait_text, "HTML");
let result = api.run_speedtest();
let text = "⚡ <b>Результат Speedtest</b>\n\n";
let text = "⚡ <b>" + t("speed_result") + "</b>\n\n";
if (result) {
// A failed transfer yields 0 bytes/s; report that as an error instead of "0.0 Mbps"
text += "📥 Прямое соединение: " +
text += t("speed_direct") + " " +
(result.direct_mbps > 0
? "<code>" + sprintf("%.1f", result.direct_mbps) + " Mbps</code>\n"
: "❌ не удалось измерить\n");
text += "📤 Через прокси: " +
: t("speed_fail_measure") + "\n");
text += t("speed_proxy") + " " +
(result.proxy_mbps > 0
? "<code>" + sprintf("%.1f", result.proxy_mbps) + " Mbps</code>\n"
: "❌ не удалось измерить\n");
: t("speed_fail_measure") + "\n");
if (result.direct_mbps <= 0 && result.proxy_mbps <= 0)
text += "\n<i>Проверьте доступ в интернет и работу sing-box.</i>\n";
text += "\n<i>" + t("speed_check_hint") + "</i>\n";
} else {
text += "❌ Не удалось выполнить тест. Проверьте, запущен ли sing-box.\n";
text += t("speed_error") + "\n";
}
let keyboard = [
[{ text: "⚡ Ещё раз", callback_data: "/speed" }],
[{ text: "⬅️ Назад", callback_data: "/status" }]
[{ text: t("btn_speed_again"), callback_data: "/speed" }],
[{ text: t("nav_back"), callback_data: "/status" }]
];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
}
function view_ping(token, chat_id, msg_id) {
let text = "📍 <b>Задержка до серверов</b>\n\n";
let text = "📍 <b>" + t("menu_ping") + "</b>\n\n";
let data = api.get_clash_proxies_data();
if (!data || !data.proxies) {
text += "❌ Не удалось получить данные из Clash API.";
let keyboard = [[{ text: "🔄 Обновить", callback_data: "/ping" }, { text: "⬅️ Назад", callback_data: "/status" }]];
text += t("ping_api_error");
let keyboard = [[{ text: t("btn_refresh"), callback_data: "/ping" }, { text: t("nav_back"), callback_data: "/status" }]];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
return;
@@ -1587,7 +1584,7 @@ function view_ping(token, chat_id, msg_id) {
text += icon + " <code>" + escape_html(name) + "</code>: <code>" + delay + " ms</code>\n";
shown++;
}
if (shown == 0) text += "Нет данных о задержках. Нажмите «Обновить».";
if (shown == 0) text += t("latency_no_data");
let keyboard = [
[{ text: "🔄 Обновить", callback_data: "/ping" }],
@@ -1600,19 +1597,19 @@ function view_ping(token, chat_id, msg_id) {
// ─── Phase 2: Quick Test, Logs, System Info ────────────────────────────────
function view_quick_test(token, chat_id, msg_id) {
let text = "🩺 <b>Быстрая диагностика</b>\n\n";
let text = "🩺 <b>" + t("test_title") + "</b>\n\n";
let conn = api.check_connection();
let sys = api.get_system_status();
text += (conn && conn.direct ? "✅" : "❌") + " Прямое соединение\n";
text += (conn && conn.proxy ? "✅" : "❌") + " Прокси соединение\n";
text += (sys && sys.singbox_running ? "✅" : "❌") + " sing-box процесс\n";
text += (sys && sys.tachyon_running ? "✅" : "❌") + " Tachyon сервис\n";
text += (conn && conn.direct ? "✅" : "❌") + " " + t("test_direct") + "\n";
text += (conn && conn.proxy ? "✅" : "❌") + " " + t("test_proxy") + "\n";
text += (sys && sys.singbox_running ? "✅" : "❌") + " sing-box\n";
text += (sys && sys.tachyon_running ? "✅" : "❌") + " " + t("test_service") + "\n";
text += (sys && sys.watchdog_running ? "✅" : "❌") + " Watchdog\n";
let nft = command_capture(command_from_args(["/usr/sbin/nft", "list", "tables"]));
text += (nft && nft.status == 0 && match(nft.output || "", /ip tachyon/)) ? "✅" : "❌";
text += " nftables правила\n";
text += " " + t("test_nft") + "\n";
let dns_ok = false;
try {
@@ -1620,19 +1617,16 @@ function view_quick_test(token, chat_id, msg_id) {
dns_ok = dns_res && dns_res.status == 0;
}
catch (e) {
// dns_ok stays false, which is what the report line below prints. A
// failing DNS check and a failing nslookup invocation mean the same
// thing to the reader.
}
text += (dns_ok ? "✅" : "❌") + " DNS на роутере\n";
text += (dns_ok ? "✅" : "❌") + " DNS\n";
if (sys && sys.pause_remaining > 0) {
text += "\n⏸ Пауза активна: " + as_string(sys.pause_remaining) + " сек.";
text += "\n⏸ " + t("test_pause") + " " + as_string(sys.pause_remaining) + " sec.";
}
let keyboard = [
[{ text: "🔄 Повторить", callback_data: "/test" }],
[{ text: "⬅️ Назад", callback_data: "/menu" }]
[{ text: t("btn_retry"), callback_data: "/test" }],
[{ text: t("nav_back"), callback_data: "/menu" }]
];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
@@ -1661,9 +1655,9 @@ function view_logs(token, chat_id, msg_id, level, count) {
for (let i = start; i < length(filtered); i++) {
text += escape_html(filtered[i]) + "\n";
}
if (length(filtered) == 0) text += "Нет записей для данного уровня.\n";
if (length(filtered) == 0) text += t("logs_no_records");
} else {
text += "Не удалось прочитать логи.\n";
text += t("logs_read_error");
}
text += "</pre>";
@@ -1722,22 +1716,22 @@ function view_connections(token, chat_id, msg_id, page) {
page = int(page || 0);
if (page < 0) page = 0;
let data = api.get_clash_connections();
let text = "🔗 <b>Активные подключения</b>\n\n";
let text = "🔗 <b>" + t("menu_connections") + "</b>\n\n";
if (!data || !data.connections || length(data.connections) == 0) {
text += "Нет активных подключений.";
text += t("conn_none");
let keyboard = [
[{ text: "🔄 Обновить", callback_data: "/connections" }],
[{ text: "⬅️ Назад", callback_data: "/menu" }]
[{ text: t("btn_refresh"), callback_data: "/connections" }],
[{ text: t("nav_back"), callback_data: "/menu" }]
];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
return;
}
text += "📥 Всего скачано: <code>" + format_bytes(data.downloadTotal) + "</code>\n";
text += "📤 Всего отдано: <code>" + format_bytes(data.uploadTotal) + "</code>\n";
text += "🔗 Соединений: <code>" + length(data.connections) + "</code>\n\n";
text += t("conn_total_down") + ": <code>" + format_bytes(data.downloadTotal) + "</code>\n";
text += t("conn_total_up") + ": <code>" + format_bytes(data.uploadTotal) + "</code>\n";
text += t("stat_connections") + ": <code>" + length(data.connections) + "</code>\n\n";
let per_page = 8;
let total = length(data.connections);
@@ -1768,8 +1762,8 @@ function view_connections(token, chat_id, msg_id, page) {
if (end < total) push(nav, { text: "▶️ Next", callback_data: "/connections " + as_string(page + 1) });
push(keyboard, nav);
}
push(keyboard, [{ text: "🔄 Обновить", callback_data: "/connections" }]);
push(keyboard, [{ text: "❌ Закрыть все", callback_data: "/close_connections" }]);
push(keyboard, [{ text: t("btn_refresh"), callback_data: "/connections" }]);
push(keyboard, [{ text: t("btn_close_all"), callback_data: "/close_connections" }]);
push(keyboard, [{ text: "⬅️ Назад", callback_data: "/menu" }]);
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
@@ -1801,7 +1795,7 @@ function view_sec_subs(token, chat_id, msg_id, sec_name) {
push(keyboard, [{ text: "🔙 Назад", callback_data: "/sec_view " + sec_name }]);
let text = "🔗 <b>Подписки секции " + escape_html(sec_name) + "</b>\n\n";
if (count == 0) {
text += "Нет подписок. Добавьте URL подписки для скачивания правил.";
text += t("sub_none");
} else {
text += "Найдено подписок: " + count;
}
@@ -1897,7 +1891,39 @@ function view_help(token, chat_id, msg_id) {
else send_message(token, chat_id, text, "HTML", keyboard);
}
// ─── Phase 4: Quiet Hours, Rule Test, Export ───────────────────────────────
// ─── Phase 4: Language, Quiet Hours, Rule Test, Export ───────────────────────
function view_language(token, chat_id, msg_id) {
let c = uci_core.cursor();
c.load(CONFIG_NAME);
let current_lang = option(c.get_all(CONFIG_NAME, "telegram"), "language", "en");
let langs = i18n.available_languages(current_lang);
let text = t("choose_language") + "\n\n" + t("lang_current", current_lang);
let keyboard = [];
let row = [];
for (let i = 0; i < length(langs); i++) {
let l = langs[i];
let marker = (l.code == current_lang) ? " ✅" : "";
push(row, { text: l.label + marker, callback_data: "/lang_set " + l.code });
if (length(row) == 2) { push(keyboard, row); row = []; }
}
if (length(row) > 0) push(keyboard, row);
push(keyboard, [{ text: t("nav_back"), callback_data: "/settings" }]);
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard);
}
function handle_lang_set(token, chat_id, msg_id, lang) {
lang = i18n.resolve_lang(lang);
let c = uci_core.cursor();
c.load(CONFIG_NAME);
c.set(CONFIG_NAME, "telegram", "language", lang);
c.commit(CONFIG_NAME);
return send_message(token, chat_id, t("lang_saved") + " " + t("lang_current", lang), "HTML",
[[{ text: t("nav_back"), callback_data: "/settings" }]]);
}
function view_quiet_hours(token, chat_id, msg_id) {
let c = uci_core.cursor();
@@ -2150,7 +2176,7 @@ function exec_ai_heal(token, chat_id, msg_id) {
if (st.last_incident) {
text += "⚠️ <b>Устранён инцидент:</b> " + escape_html(st.last_incident.description || "") + "\n";
text += "🔧 <b>Авто-решение:</b> " + escape_html(st.last_incident.resolution || "") + "\n\n";
text += "Все системы приведены в штатную норму! 🟢";
text += t("status_normal");
} else {
text += "🟢 Все сетевые службы, DNS, nftables и память работают идеально.";
}
@@ -2251,6 +2277,11 @@ function dispatch_command(token, chat_id, text, msg_id) {
if (cmd == "/test") return view_quick_test(token, chat_id, msg_id);
if (cmd == "/info") return view_system_info(token, chat_id, msg_id);
if (cmd == "/help") return view_help(token, chat_id, msg_id);
if (cmd == "/lang") return view_language(token, chat_id, msg_id);
if (match(cmd, /^\/lang_set /)) {
let lang = trim(substr(cmd, 11));
return handle_lang_set(token, chat_id, msg_id, lang);
}
if (match(cmd, /^\/logs /)) {
let parts = split(trim(substr(cmd, 6)), " ");
@@ -2981,24 +3012,25 @@ function worker() {
if (cfg.enabled != "1" || !cfg.bot_token) return 0;
let commands = [
{ command: "menu", description: "Главное меню" },
{ command: "status", description: "Статус системы" },
{ command: "runtime", description: "Статистика трафика" },
{ command: "outbounds", description: "Прокси серверы" },
{ command: "sections", description: "Секции маршрутизации" },
{ command: "instances", description: "Live серверы" },
{ command: "speed", description: "Тест скорости" },
{ command: "ping", description: "Задержка до серверов" },
{ command: "test", description: "Быстрая диагностика" },
{ command: "logs", description: "Просмотр логов" },
{ command: "info", description: "Информация о системе" },
{ command: "connections", description: "Активные подключения" },
{ command: "test_rule", description: "Проверка правила" },
{ command: "help", description: "Справка" },
{ command: "check_updates", description: "Проверить обновления" },
{ command: "close_connections", description: "Закрыть все соединения" },
{ command: "doctor", description: "Диагностика" },
{ command: "restart", description: "Перезапуск служб Tachyon" }
{ command: "menu", description: t("cmd_menu") },
{ command: "status", description: t("cmd_status") },
{ command: "runtime", description: t("cmd_runtime") },
{ command: "outbounds", description: t("cmd_outbounds") },
{ command: "sections", description: t("cmd_sections") },
{ command: "instances", description: t("cmd_instances") },
{ command: "speed", description: t("cmd_speed") },
{ command: "ping", description: t("cmd_ping") },
{ command: "test", description: t("cmd_test") },
{ command: "logs", description: t("cmd_logs") },
{ command: "info", description: t("cmd_info") },
{ command: "connections", description: t("cmd_connections") },
{ command: "test_rule", description: t("cmd_test_rule") },
{ command: "help", description: t("cmd_help") },
{ command: "check_updates", description: t("cmd_check_updates") },
{ command: "close_connections", description: t("cmd_close_connections") },
{ command: "doctor", description: t("cmd_doctor") },
{ command: "restart", description: t("cmd_restart") },
{ command: "lang", description: t("cmd_lang") }
];
tg_request(cfg.bot_token, "setMyCommands", { commands: commands });
@@ -3028,6 +3060,7 @@ function worker() {
try {
cfg = settings();
if (cfg.enabled != "1") break;
t = i18n.bind(cfg.language);
let res = process_updates(cfg.bot_token, cfg.admin_ids);
if (res === false) {