feat(telegram): расширение функционала бота (мониторинг, диагностика, отказоустойчивость)

- Добавлена генерация Support Bundle (/support_bundle)

- Добавлен мгновенный сброс соединений (/close_connections)

- Добавлен просмотр Live Server Instances (/instances)

- Добавлено автоматическое добавление админов через forward сообщений

- Реализованы Тихие часы для некритичных уведомлений

- Реализована отправка утреннего дайджеста о трафике и аптайме

- Добавлен мониторинг смены активного узла в URLTest

- Добавлен резервный SOCKS5 транспорт
This commit is contained in:
Dushnilin
2026-07-21 09:35:50 +03:00
parent bf24983bc4
commit 63adf59e8d
2 changed files with 259 additions and 4 deletions
+237 -4
View File
@@ -25,9 +25,13 @@ function settings() {
}
function get_proxy_args() {
let cfg = settings();
if (command_success_from_args(["pidof", "sing-box"])) {
return [ "--proxy", "http://127.0.0.1:4534" ];
}
if (cfg.fallback_socks && trim(cfg.fallback_socks) != "") {
return [ "--proxy", "socks5h://" + trim(cfg.fallback_socks) ];
}
return [];
}
@@ -70,6 +74,26 @@ function edit_message(token, chat_id, message_id, text, parse_mode, keyboard) {
return tg_request(token, "editMessageText", payload);
}
function send_document(token, chat_id, file_path) {
if (!token) return null;
let url = "https://api.telegram.org/bot" + token + "/sendDocument";
let args = [ "curl", "-s", "-X", "POST", "-F", "chat_id=" + chat_id, "-F", "document=@" + file_path ];
let proxy = get_proxy_args();
for (let p in proxy) push(args, p);
push(args, url);
let res = command_capture(command_from_args(args));
if (!res || res.status != 0 || res.output == "") return null;
try { return json(res.output); } catch (e) { return null; }
}
function get_file_url(token, file_id) {
let res = tg_request(token, "getFile", { file_id: file_id });
if (res && res.ok && res.result && res.result.file_path) {
return "https://api.telegram.org/file/bot" + token + "/" + res.result.file_path;
}
return null;
}
// ─── State Management ────────────────────────────────────────────────────────
function get_tg_state(chat_id) {
@@ -168,6 +192,9 @@ let setting_schema = {
},
server: {
label: "Название",
daily_report_enabled: "Ежедневный отчет",
daily_report_hour: "Время отчета (час)",
fallback_socks: "Резервный SOCKS5",
enabled: "Включен",
protocol: "Протокол",
routing_mode: "Режим"
@@ -734,6 +761,79 @@ function exec_restart(token, chat_id) {
else send_message(token, chat_id, "❌ <b>Ошибка при перезапуске.</b>", "HTML", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
}
function exec_backup(token, chat_id) {
send_message(token, chat_id, "⏳ <b>Собираю бэкап...</b>", "HTML");
let file_path = "/tmp/tachyon_backup.tar.gz";
let cmd = "tar -czf " + file_path + " -C /etc config/tachyon tachyon 2>/dev/null";
command_status(cmd);
if (fs.stat(file_path)) {
send_document(token, chat_id, file_path);
fs.unlink(file_path);
} else {
send_message(token, chat_id, "❌ <b>Ошибка создания бэкапа.</b>", "HTML", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
}
}
function exec_support_bundle(token, chat_id) {
send_message(token, chat_id, "⏳ <b>Формирую Support Bundle...</b>", "HTML");
command_status("ip route > /tmp/tachyon_ip_route.txt");
command_status("logread > /tmp/tachyon_logread.txt");
let file_path = "/tmp/support_bundle.tar.gz";
let cmd = "tar -czf " + file_path + " /etc/config/tachyon /var/etc/tachyon /etc/config/network /etc/config/firewall /tmp/dhcp.leases /tmp/tachyon_ip_route.txt /tmp/tachyon_logread.txt 2>/dev/null";
command_status(cmd);
if (fs.stat(file_path)) {
send_document(token, chat_id, file_path);
fs.unlink(file_path);
} else {
send_message(token, chat_id, "❌ <b>Ошибка генерации Bundle.</b>", "HTML");
}
try { fs.unlink("/tmp/tachyon_ip_route.txt"); fs.unlink("/tmp/tachyon_logread.txt"); } catch(e) {}
}
function exec_close_connections(token, chat_id) {
let out = command_capture(command_from_args(["curl", "-s", "-X", "DELETE", "http://127.0.0.1:4534/connections"]));
send_message(token, chat_id, "✅ <b>Все активные соединения сброшены.</b>\nОни будут переустановлены по новым маршрутам.", "HTML", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
}
function view_instances(token, chat_id, msg_id) {
let res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:4534/proxies"]));
let text = "🖧 <b>Live Server Instances</b>\n\n";
if (res && res.status == 0 && res.output) {
try {
let data = json(res.output);
let proxies = data.proxies;
let count = 0;
for (let name in proxies) {
let p = proxies[name];
if (p.type == "Selector" || p.type == "URLTest" || p.type == "Direct" || p.type == "Reject" || p.type == "Compatible") continue;
let delay = "➖";
if (p.history && length(p.history) > 0) {
let last = p.history[length(p.history) - 1];
if (last.delay > 0) delay = last.delay + " ms";
else delay = "❌ Timeout";
}
text += "• <code>" + escape_html(name) + "</code> (" + p.type + "): <b>" + delay + "</b>\n";
count++;
}
if (count == 0) text += "Серверы не найдены или sing-box не запущен.";
} catch(e) {
text += "Ошибка парсинга API: " + e;
}
} else {
text += "❌ Не удалось подключиться к API sing-box.";
}
let kb = [[
{text: "🔄 Обновить", callback_data: "/instances"},
{text: "⬅️ Назад", callback_data: "/status"}
]];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", kb);
else send_message(token, chat_id, text, "HTML", kb);
}
function view_devices(token, chat_id, msg_id) {
let lease_file = "/tmp/dhcp.leases";
let data = fs.readfile(lease_file);
@@ -820,6 +920,31 @@ function dispatch_command(token, chat_id, text, msg_id) {
if (cmd == "/watchdog") return view_watchdog(token, chat_id, msg_id);
if (cmd == "/doctor") return exec_doctor(token, chat_id);
if (cmd == "/restart") return exec_restart(token, chat_id);
if (cmd == "/backup") return exec_backup(token, chat_id);
if (cmd == "/support_bundle") return exec_support_bundle(token, chat_id);
if (cmd == "/close_connections") return exec_close_connections(token, chat_id);
if (cmd == "/instances") return view_instances(token, chat_id, msg_id);
if (match(cmd, /^\/admin_add /)) {
let fwd_id = trim(substr(cmd, 11));
let c = uci_core.cursor(); c.load(CONFIG_NAME);
let s = c.get_all(CONFIG_NAME, "telegram");
let current_admins = option(s, "admin_ids", "");
let admins_list = split(current_admins, /,/);
let found = false;
for (let a in admins_list) if (trim(a) == fwd_id) found = true;
if (!found) {
let new_admins = current_admins != "" ? current_admins + "," + fwd_id : fwd_id;
c.set(CONFIG_NAME, "telegram", "admin_ids", new_admins);
c.commit(CONFIG_NAME);
if (msg_id) edit_message(token, chat_id, msg_id, "✅ Пользователь `" + fwd_id + "` добавлен в список администраторов.", "Markdown", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
else send_message(token, chat_id, "✅ Пользователь `" + fwd_id + "` добавлен в список администраторов.", "Markdown", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
} else {
if (msg_id) edit_message(token, chat_id, msg_id, "ℹ️ Пользователь `" + fwd_id + "` уже является администратором.", "Markdown", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
}
return;
}
if (cmd == "/wd_start") {
command_status("/usr/bin/tachyon watchdog_start");
@@ -999,14 +1124,68 @@ function process_updates(token, admin_ids) {
}
let msg = upd.message;
if (msg && msg.text) {
let chat_id = msg.chat.id;
if (msg) {
let chat_id = msg.chat ? msg.chat.id : null;
if (!chat_id) continue;
if (!is_admin(chat_id, admin_ids)) {
send_message(token, chat_id, "❌ Доступ запрещен. Ваш Chat ID: `" + chat_id + "`", "Markdown");
if (msg.text || msg.document) {
send_message(token, chat_id, "❌ Доступ запрещен. Ваш Chat ID: `" + chat_id + "`", "Markdown");
}
continue;
}
if (msg.text == "/cancel") {
if (msg.document) {
let doc = msg.document;
if (match(doc.file_name || "", /\.tar\.gz$/)) {
send_message(token, chat_id, "⏳ <b>Скачиваю бэкап...</b>", "HTML");
let file_url = get_file_url(token, doc.file_id);
if (file_url) {
let dl_path = "/tmp/restore_" + doc.file_id + ".tar.gz";
let proxy = get_proxy_args();
let dl_args = [ "curl", "-s", "-o", dl_path ];
for (let p in proxy) push(dl_args, p);
push(dl_args, file_url);
command_status(command_from_args(dl_args));
if (fs.stat(dl_path)) {
send_message(token, chat_id, "🔄 <b>Восстанавливаю бэкап...</b>", "HTML");
command_status("tar -xzf " + dl_path + " -C /etc");
fs.unlink(dl_path);
send_message(token, chat_id, "✅ <b>Бэкап восстановлен. Перезапускаю Tachyon...</b>", "HTML");
command_status("/usr/bin/tachyon restart");
send_message(token, chat_id, "✅ <b>Успешно!</b>", "HTML", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
} else {
send_message(token, chat_id, "❌ <b>Ошибка загрузки файла.</b>", "HTML");
}
} else {
send_message(token, chat_id, "❌ <b>Ошибка получения URL файла.</b>", "HTML");
}
} else {
send_message(token, chat_id, "ℹ️ Пожалуйста, отправьте бэкап в формате `.tar.gz`.", "Markdown");
}
continue;
}
if (msg.forward_from) {
let fwd_id = msg.forward_from.id;
let text = "👤 Вы переслали сообщение от пользователя `" + fwd_id + "`.\nДобавить его в список администраторов бота?";
let keyboard = [[{text: "✅ Добавить", callback_data: "/admin_add " + fwd_id}]];
send_message(token, chat_id, text, "Markdown", keyboard);
continue;
}
if (msg.text) {
if (match(msg.text, /^> /)) {
let exec_cmd = trim(substr(msg.text, 2));
send_message(token, chat_id, "⏳ Выполняю:\n`" + escape_html(exec_cmd) + "`", "HTML");
let out = command_capture(exec_cmd);
let result_text = "<b>Выполнено (код " + out.status + "):</b>\n<pre>" + escape_html(out.output || "Нет вывода") + "</pre>";
if (length(result_text) > 4000) result_text = substr(result_text, 0, 4000) + "...</pre>";
send_message(token, chat_id, result_text, "HTML");
continue;
}
if (msg.text == "/cancel") {
set_tg_state(chat_id, null);
send_message(token, chat_id, "❌ Действие отменено.", "HTML", [[{text:"⬅️ Меню", callback_data:"/menu"}]]);
continue;
@@ -1099,6 +1278,29 @@ function process_updates(token, admin_ids) {
// ─── Entry Point ─────────────────────────────────────────────────────────────
function send_daily_digest(token, admin_ids) {
let text = "📊 <b>Утренний дайджест Tachyon</b>\n\n";
let uptime_out = command_output_from_args(["uptime"]);
let m = match(uptime_out, /up ([^,]+)/);
let up = m ? m[1] : "неизвестно";
text += "⏱ Аптайм ОС: " + up + "\n";
let res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:4534/traffic"]));
if (res && res.status == 0 && res.output) {
try {
let tr = json(res.output);
text += "🔻 Текущий RX: " + format_bytes(tr.down) + "/s\n";
text += "🔺 Текущий TX: " + format_bytes(tr.up) + "/s\n";
} catch(e) {}
}
let admins = split(admin_ids, /,/);
for (let admin in admins) {
let chat_id = trim(admin);
if (chat_id != "") send_message(token, chat_id, text, "HTML", null);
}
}
function worker() {
let cfg = settings();
if (cfg.enabled != "1" || !cfg.bot_token) return 0;
@@ -1109,6 +1311,8 @@ function worker() {
{ command: "runtime", description: "Runtime stats" },
{ command: "outbounds", description: "Proxy servers" },
{ command: "sections", description: "Routing sections" },
{ command: "instances", description: "Live Server Instances" },
{ command: "close_connections", description: "Close All Connections" },
{ command: "doctor", description: "Diagnostics" },
{ command: "restart", description: "Restart router" }
];
@@ -1117,10 +1321,22 @@ function worker() {
let poll_interval = int(cfg.poll_interval || "5");
if (poll_interval < 1) poll_interval = 1;
let last_report_day = -1;
while (true) {
cfg = settings();
if (cfg.enabled != "1") break;
process_updates(cfg.bot_token, cfg.admin_ids);
let now = time();
let tm = clock(now); // [year, mon, day, hour, min, sec]
let daily_hour = int(cfg.daily_report_hour || "8");
if (cfg.daily_report_enabled == "1" && tm[3] == daily_hour && tm[2] != last_report_day) {
last_report_day = tm[2];
send_daily_digest(cfg.bot_token, cfg.admin_ids);
}
sleep(poll_interval * 1000);
}
return 0;
@@ -1155,9 +1371,26 @@ function get_status() {
return 1;
}
function in_quiet_hours(cfg) {
if (cfg.quiet_hours_enabled != "1") return false;
let start = int(cfg.quiet_hours_start || "23");
let end = int(cfg.quiet_hours_end || "7");
let hr = clock()[3];
if (start <= end) {
return hr >= start && hr < end;
} else {
return hr >= start || hr < end;
}
}
function send_api(message) {
let cfg = settings();
if (cfg.enabled != "1" || !cfg.bot_token || !cfg.admin_ids) return 1;
// Check if this is a non-critical watchdog message and we are in quiet hours
let is_critical = (index(message, "Упал") >= 0 || index(message, "Ошибка") >= 0);
if (!is_critical && in_quiet_hours(cfg)) return 0;
let admins = split(cfg.admin_ids, /,/);
for (let admin in admins) {
let chat_id = trim(admin);