telegram bot: 12 новых фич + updates panel: кнопки GitHub репозиториев

This commit is contained in:
Dushnilin
2026-08-02 10:41:04 +03:00
parent a7bbd746db
commit 1e1f19d7b4
6 changed files with 593 additions and 25 deletions
+1
View File
@@ -20,3 +20,4 @@ export * from './renderCopyIcon24';
export * from './renderLinkIcon24'; export * from './renderLinkIcon24';
export * from './renderDownloadIcon24'; export * from './renderDownloadIcon24';
export * from './renderInfoIcon24'; export * from './renderInfoIcon24';
export * from './renderGlobeIcon24';
@@ -0,0 +1,24 @@
import { svgEl } from '../helpers/svgEl';
export function renderGlobeIcon24() {
return svgEl(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
width: '14',
height: '14',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
class: 'lucide lucide-globe-icon lucide-globe',
},
[
svgEl('circle', { cx: '12', cy: '12', r: '10' }),
svgEl('path', { d: 'M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20' }),
svgEl('path', { d: 'M2 12h20' }),
],
);
}
@@ -4,6 +4,7 @@ import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVers
import { showToast } from '../../../helpers/showToast'; import { showToast } from '../../../helpers/showToast';
import { import {
renderDownloadIcon24, renderDownloadIcon24,
renderGlobeIcon24,
renderRotateCcwIcon24, renderRotateCcwIcon24,
renderSearchIcon24, renderSearchIcon24,
renderXIcon24, renderXIcon24,
@@ -59,6 +60,7 @@ interface ComponentCard {
version: string; version: string;
latestVersion?: string; latestVersion?: string;
releaseUrl?: string; releaseUrl?: string;
repoUrl?: string;
actions: ComponentActionButton[]; actions: ComponentActionButton[];
} }
@@ -818,6 +820,14 @@ function getOptionalComponentActions({
]; ];
} }
const COMPONENT_REPO_URLS: Record<Tachyon.ComponentName, string> = {
tachyon: 'https://github.com/Dushnilin/tachyon',
sing_box: 'https://github.com/SagerNet/sing-box',
zapret: 'https://github.com/remittor/zapret-openwrt',
zapret2: 'https://github.com/1andrevich/zapret2-openwrt',
byedpi: 'https://github.com/DPITrickster/ByeDPI-OpenWrt',
};
function getComponentCards(): ComponentCard[] { function getComponentCards(): ComponentCard[] {
const systemInfo = normalizeSingBoxVariantFields( const systemInfo = normalizeSingBoxVariantFields(
store.get().diagnosticsSystemInfo, store.get().diagnosticsSystemInfo,
@@ -942,6 +952,7 @@ function getComponentCards(): ComponentCard[] {
: normalizeCompiledVersion(systemInfo.tachyon_version), : normalizeCompiledVersion(systemInfo.tachyon_version),
latestVersion: getLatestVersion('tachyon'), latestVersion: getLatestVersion('tachyon'),
releaseUrl: getGitHubReleaseUrl('tachyon'), releaseUrl: getGitHubReleaseUrl('tachyon'),
repoUrl: COMPONENT_REPO_URLS.tachyon,
actions: tachyonActions, actions: tachyonActions,
}, },
{ {
@@ -953,6 +964,7 @@ function getComponentCards(): ComponentCard[] {
: formatSingBoxVersion(systemInfo), : formatSingBoxVersion(systemInfo),
latestVersion: getLatestVersion('sing_box'), latestVersion: getLatestVersion('sing_box'),
releaseUrl: getGitHubReleaseUrl('sing_box'), releaseUrl: getGitHubReleaseUrl('sing_box'),
repoUrl: COMPONENT_REPO_URLS.sing_box,
actions: singBoxActions, actions: singBoxActions,
}, },
{ {
@@ -966,6 +978,7 @@ function getComponentCards(): ComponentCard[] {
: _('Not installed'), : _('Not installed'),
latestVersion: getLatestVersion('zapret'), latestVersion: getLatestVersion('zapret'),
releaseUrl: getGitHubReleaseUrl('zapret'), releaseUrl: getGitHubReleaseUrl('zapret'),
repoUrl: COMPONENT_REPO_URLS.zapret,
actions: zapretActions, actions: zapretActions,
}, },
{ {
@@ -979,6 +992,7 @@ function getComponentCards(): ComponentCard[] {
: _('Not installed'), : _('Not installed'),
latestVersion: getLatestVersion('zapret2'), latestVersion: getLatestVersion('zapret2'),
releaseUrl: getGitHubReleaseUrl('zapret2'), releaseUrl: getGitHubReleaseUrl('zapret2'),
repoUrl: COMPONENT_REPO_URLS.zapret2,
actions: zapret2Actions, actions: zapret2Actions,
}, },
{ {
@@ -992,6 +1006,7 @@ function getComponentCards(): ComponentCard[] {
: _('Not installed'), : _('Not installed'),
latestVersion: getLatestVersion('byedpi'), latestVersion: getLatestVersion('byedpi'),
releaseUrl: getGitHubReleaseUrl('byedpi'), releaseUrl: getGitHubReleaseUrl('byedpi'),
repoUrl: COMPONENT_REPO_URLS.byedpi,
actions: byedpiActions, actions: byedpiActions,
}, },
]; ];
@@ -1011,6 +1026,21 @@ function renderComponentCard(card: ComponentCard) {
card.version, card.version,
), ),
]; ];
if (card.repoUrl) {
headerChildren.push(
E(
'a',
{
class: 'tachyon_updates-page__component__repo-link',
href: card.repoUrl,
target: '_blank',
rel: 'noopener noreferrer',
title: card.repoUrl,
},
renderGlobeIcon24(),
),
);
}
const header = E( const header = E(
'div', 'div',
{ class: 'tachyon_updates-page__component__header' }, { class: 'tachyon_updates-page__component__header' },
@@ -74,6 +74,24 @@ export const styles = `
font-weight: normal; font-weight: normal;
} }
.tachyon_updates-page__component__repo-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 3px;
color: var(--text-color-medium, #888);
text-decoration: none;
transition: color 0.15s, background-color 0.15s;
flex-shrink: 0;
}
.tachyon_updates-page__component__repo-link:hover {
color: var(--link-color, #3498db);
background-color: var(--background-color-low, rgba(0, 0, 0, 0.05));
}
.tachyon_updates-page__component__details { .tachyon_updates-page__component__details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1908,6 +1908,30 @@ function renderInfoIcon24() {
); );
} }
// src/icons/renderGlobeIcon24.ts
function renderGlobeIcon24() {
return svgEl(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "14",
height: "14",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "lucide lucide-globe-icon lucide-globe"
},
[
svgEl("circle", { cx: "12", cy: "12", r: "10" }),
svgEl("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }),
svgEl("path", { d: "M2 12h20" })
]
);
}
// src/helpers/prettyBytes.ts // src/helpers/prettyBytes.ts
function prettyBytes(n) { function prettyBytes(n) {
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
@@ -14178,6 +14202,13 @@ function getOptionalComponentActions({
} }
]; ];
} }
var COMPONENT_REPO_URLS = {
tachyon: "https://github.com/Dushnilin/tachyon",
sing_box: "https://github.com/SagerNet/sing-box",
zapret: "https://github.com/remittor/zapret-openwrt",
zapret2: "https://github.com/1andrevich/zapret2-openwrt",
byedpi: "https://github.com/DPITrickster/ByeDPI-OpenWrt"
};
function getComponentCards() { function getComponentCards() {
const systemInfo = normalizeSingBoxVariantFields( const systemInfo = normalizeSingBoxVariantFields(
store.get().diagnosticsSystemInfo store.get().diagnosticsSystemInfo
@@ -14286,6 +14317,7 @@ function getComponentCards() {
version: systemInfoLoading ? _("Loading...") : normalizeCompiledVersion(systemInfo.tachyon_version), version: systemInfoLoading ? _("Loading...") : normalizeCompiledVersion(systemInfo.tachyon_version),
latestVersion: getLatestVersion("tachyon"), latestVersion: getLatestVersion("tachyon"),
releaseUrl: getGitHubReleaseUrl("tachyon"), releaseUrl: getGitHubReleaseUrl("tachyon"),
repoUrl: COMPONENT_REPO_URLS.tachyon,
actions: tachyonActions actions: tachyonActions
}, },
{ {
@@ -14295,6 +14327,7 @@ function getComponentCards() {
version: systemInfoLoading ? _("Loading...") : formatSingBoxVersion(systemInfo), version: systemInfoLoading ? _("Loading...") : formatSingBoxVersion(systemInfo),
latestVersion: getLatestVersion("sing_box"), latestVersion: getLatestVersion("sing_box"),
releaseUrl: getGitHubReleaseUrl("sing_box"), releaseUrl: getGitHubReleaseUrl("sing_box"),
repoUrl: COMPONENT_REPO_URLS.sing_box,
actions: singBoxActions actions: singBoxActions
}, },
{ {
@@ -14304,6 +14337,7 @@ function getComponentCards() {
version: systemInfoLoading ? _("Loading...") : zapretInstalled ? systemInfo.zapret_version : _("Not installed"), version: systemInfoLoading ? _("Loading...") : zapretInstalled ? systemInfo.zapret_version : _("Not installed"),
latestVersion: getLatestVersion("zapret"), latestVersion: getLatestVersion("zapret"),
releaseUrl: getGitHubReleaseUrl("zapret"), releaseUrl: getGitHubReleaseUrl("zapret"),
repoUrl: COMPONENT_REPO_URLS.zapret,
actions: zapretActions actions: zapretActions
}, },
{ {
@@ -14313,6 +14347,7 @@ function getComponentCards() {
version: systemInfoLoading ? _("Loading...") : zapret2Installed ? systemInfo.zapret2_version : _("Not installed"), version: systemInfoLoading ? _("Loading...") : zapret2Installed ? systemInfo.zapret2_version : _("Not installed"),
latestVersion: getLatestVersion("zapret2"), latestVersion: getLatestVersion("zapret2"),
releaseUrl: getGitHubReleaseUrl("zapret2"), releaseUrl: getGitHubReleaseUrl("zapret2"),
repoUrl: COMPONENT_REPO_URLS.zapret2,
actions: zapret2Actions actions: zapret2Actions
}, },
{ {
@@ -14322,6 +14357,7 @@ function getComponentCards() {
version: systemInfoLoading ? _("Loading...") : byedpiInstalled ? systemInfo.byedpi_version : _("Not installed"), version: systemInfoLoading ? _("Loading...") : byedpiInstalled ? systemInfo.byedpi_version : _("Not installed"),
latestVersion: getLatestVersion("byedpi"), latestVersion: getLatestVersion("byedpi"),
releaseUrl: getGitHubReleaseUrl("byedpi"), releaseUrl: getGitHubReleaseUrl("byedpi"),
repoUrl: COMPONENT_REPO_URLS.byedpi,
actions: byedpiActions actions: byedpiActions
} }
]; ];
@@ -14338,6 +14374,21 @@ function renderComponentCard(card) {
card.version card.version
) )
]; ];
if (card.repoUrl) {
headerChildren.push(
E(
"a",
{
class: "tachyon_updates-page__component__repo-link",
href: card.repoUrl,
target: "_blank",
rel: "noopener noreferrer",
title: card.repoUrl
},
renderGlobeIcon24()
)
);
}
const header = E( const header = E(
"div", "div",
{ class: "tachyon_updates-page__component__header" }, { class: "tachyon_updates-page__component__header" },
@@ -14701,6 +14752,24 @@ var styles6 = `
font-weight: normal; font-weight: normal;
} }
.tachyon_updates-page__component__repo-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 3px;
color: var(--text-color-medium, #888);
text-decoration: none;
transition: color 0.15s, background-color 0.15s;
flex-shrink: 0;
}
.tachyon_updates-page__component__repo-link:hover {
color: var(--link-color, #3498db);
background-color: var(--background-color-low, rgba(0, 0, 0, 0.05));
}
.tachyon_updates-page__component__details { .tachyon_updates-page__component__details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+451 -25
View File
@@ -367,6 +367,9 @@ function view_settings_menu(token, chat_id, msg_id) {
[{ text: "🔗 Подписки", callback_data: "/set_list subscription_url" }], [{ text: "🔗 Подписки", callback_data: "/set_list subscription_url" }],
[{ text: "🖥 Кастомные серверы", callback_data: "/set_list server" }], [{ text: "🖥 Кастомные серверы", callback_data: "/set_list server" }],
[{ text: "🌐 DNS Серверы (Пресеты)", callback_data: "/dns_presets" }], [{ 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: "⬅️ Назад", callback_data: "/menu" }]
]; ];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard); if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
@@ -527,16 +530,25 @@ function view_menu(token, chat_id, msg_id) {
{ text: "💻 Устройства", callback_data: "/devices" }, { text: "💻 Устройства", callback_data: "/devices" },
{ text: "🐕 Watchdog", callback_data: "/watchdog" } { text: "🐕 Watchdog", callback_data: "/watchdog" }
], ],
[
{ text: "⚡ Speedtest", callback_data: "/speed" },
{ text: "📍 Ping", callback_data: "/ping" }
],
[
{ text: "🩺 Диагностика", callback_data: "/test" },
{ text: "🔗 Подключения", callback_data: "/connections" }
],
[
{ text: "📋 Логи", callback_data: "/logs" },
{ text: "ℹ️ Инфо", callback_data: "/info" }
],
[ [
{ text: "🤖 ИИ-Самолечение", callback_data: "/heal" }, { text: "🤖 ИИ-Самолечение", callback_data: "/heal" },
{ text: "🎮 Игровой QoS", callback_data: "/qos" } { text: "🎮 Игровой QoS", callback_data: "/qos" }
], ],
[ [
{ text: "🩺 Диагностика", callback_data: "/doctor" }, { text: "⚙️ Все Настройки", callback_data: "/settings" },
{ text: "🔄 Перезапуск", callback_data: "/restart" } { text: "📖 Справка", callback_data: "/help" }
],
[
{ text: "⚙️ Все Настройки", callback_data: "/settings" }
] ]
]; ];
@@ -546,33 +558,46 @@ function view_menu(token, chat_id, msg_id) {
function view_status(token, chat_id, msg_id) { function view_status(token, chat_id, msg_id) {
let sys = api.get_system_status(); let sys = api.get_system_status();
let conn = api.check_connection();
let text = "📊 <b>Статус Системы</b>\n\n" + let text = "📊 <b>Статус Системы</b>\n\n" +
"Версия: <code>" + (sys.tachyon_version || "?") + "</code>\n" +
"Аптайм: <code>" + sys.uptime + "</code>\n" + "Аптайм: <code>" + sys.uptime + "</code>\n" +
"CPU: <code>" + sys.cpu + "</code>\n" + "CPU: <code>" + sys.cpu + "</code>\n" +
"RAM: <code>" + sys.ram_avail + "MB free / " + sys.ram_total + "MB total</code>\n\n" + "RAM: <code>" + sys.ram_avail + "MB / " + sys.ram_total + "MB</code>\n\n" +
"sing-box: <code>" + sys.singbox + "</code>\n" + "sing-box: <code>" + sys.singbox + "</code>\n" +
"Watchdog: <code>" + (sys.watchdog_running ? "🟢 running" : "🔴 stopped") + "</code>\n\n" + "Watchdog: <code>" + (sys.watchdog_running ? "🟢 running" : "🔴 stopped") + "</code>\n\n" +
"WAN IP: <code>" + (sys.wan_ip || "unknown") + "</code>\n" + "WAN: <code>" + (sys.wan_ip || "?") + "</code> " +
"LAN IP: <code>" + (sys.lan_ip || "unknown") + "</code>\n\n"; (conn && conn.direct ? "✅" : "❌") + "\n" +
"Proxy: " + (conn && conn.proxy ? "✅ reachable" : "❌ unreachable") + "\n" +
"LAN: <code>" + (sys.lan_ip || "?") + "</code>\n";
if (sys.pause_remaining > 0) {
text += "\n⏸ Пауза: " + as_string(sys.pause_remaining) + " сек.\n";
}
let keys_servers = keys(sys.active_servers || {}); let keys_servers = keys(sys.active_servers || {});
if (length(keys_servers) > 0) { if (length(keys_servers) > 0) {
text += "Активные серверы:\n"; text += "\nАктивные серверы:\n";
for (let i = 0; i < length(keys_servers); i++) { for (let i = 0; i < length(keys_servers); i++) {
let gname = keys_servers[i]; let gname = keys_servers[i];
let srv = sys.active_servers[gname]; let srv = sys.active_servers[gname];
let lat = srv.latency != "N/A" ? " (" + srv.latency + " ms)" : ""; let lat = srv.latency != "N/A" ? " (" + srv.latency + " ms)" : "";
text += "└ " + escape_html(gname) + ": <code>" + escape_html(srv.server) + lat + "</code>\n"; text += "└ " + escape_html(gname) + ": <code>" + escape_html(srv.server) + lat + "</code>\n";
} }
} else {
text += "Активный сервер: <code>нет</code>";
} }
let keyboard = [ let keyboard = [
[{ text: "🔄 Обновить", callback_data: "/status" }], [
{ text: "⚡ Speedtest", callback_data: "/speed" },
{ text: "📍 Ping", callback_data: "/ping" }
],
[
{ text: "🔄 Обновить", callback_data: "/status" },
{ text: "🩺 Тест", callback_data: "/test" }
],
[{ text: "⬅️ Назад", callback_data: "/menu" }] [{ text: "⬅️ Назад", callback_data: "/menu" }]
]; ];
if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard); if (msg_id) edit_message(token, chat_id, msg_id, text, "HTML", keyboard);
else send_message(token, chat_id, text, "HTML", keyboard); else send_message(token, chat_id, text, "HTML", keyboard);
} }
@@ -1163,6 +1188,373 @@ function view_instances(token, chat_id, msg_id) {
else send_message(token, chat_id, text, "HTML", kb); else send_message(token, chat_id, text, "HTML", kb);
} }
// ─── Phase 1: Speed, Ping, Improved Status ─────────────────────────────────
function exec_speedtest(token, chat_id, msg_id) {
let wait_text = "⚡ <b>Запуск Speedtest...</b>\n\nТестирую скорость через Cloudflare. Это займёт до 30 секунд.";
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";
if (result) {
text += "📥 Прямое соединение: <code>" + sprintf("%.1f", result.direct_mbps || 0) + " Mbps</code>\n";
text += "📤 Через прокси: <code>" + sprintf("%.1f", result.proxy_mbps || 0) + " Mbps</code>\n";
} else {
text += "❌ Не удалось выполнить тест. Проверьте, запущен ли sing-box.\n";
}
let keyboard = [
[{ text: "⚡ Ещё раз", callback_data: "/speed" }],
[{ text: "⬅️ Назад", 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 data = api.get_clash_proxies_data();
if (!data || !data.proxies) {
text += "❌ Не удалось получить данные из Clash API.";
let keyboard = [[{ text: "🔄 Обновить", callback_data: "/ping" }, { text: "⬅️ Назад", 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;
}
let shown = 0;
for (let name in data.proxies) {
let p = data.proxies[name];
if (p.type != "Selector" && p.type != "URLTest") continue;
if (!p.history || length(p.history) == 0) continue;
let last = p.history[length(p.history) - 1];
if (!last || !last.delay) continue;
let delay = int(last.delay);
let icon = delay < 100 ? "🟢" : (delay < 300 ? "🟡" : "🔴");
text += icon + " <code>" + escape_html(name) + "</code>: <code>" + delay + " ms</code>\n";
shown++;
}
if (shown == 0) text += "Нет данных о задержках. Нажмите «Обновить».";
let keyboard = [
[{ text: "🔄 Обновить", callback_data: "/ping" }],
[{ text: "⬅️ Назад", 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);
}
// ─── Phase 2: Quick Test, Logs, System Info ────────────────────────────────
function view_quick_test(token, chat_id, msg_id) {
let text = "🩺 <b>Быстрая диагностика</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 += (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";
let dns_ok = false;
try {
let dns_res = command_capture(command_from_args(["nslookup", "google.com", "127.0.0.1"]));
dns_ok = dns_res && dns_res.status == 0;
} catch(e) {}
text += (dns_ok ? "✅" : "❌") + " DNS на роутере\n";
if (sys && sys.pause_remaining > 0) {
text += "\n⏸ Пауза активна: " + as_string(sys.pause_remaining) + " сек.";
}
let keyboard = [
[{ text: "🔄 Повторить", callback_data: "/test" }],
[{ text: "⬅️ Назад", 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);
}
function view_logs(token, chat_id, msg_id, level, count) {
level = level || "all";
count = int(count || "30");
let text = "📋 <b>Логи Tachyon</b> (уровень: " + level + ", последние " + count + ")\n\n<pre>";
let args = ["/sbin/logread"];
let res = command_capture(command_from_args(args));
if (res && res.status == 0 && res.output) {
let lines = split(res.output, "\n");
let filtered = [];
for (let line in lines) {
if (line == "") continue;
if (level == "error" && !match(line, /\[err\]|\[error\]/i)) continue;
if (level == "warn" && !match(line, /\[warn\]/i)) continue;
if (level == "info" && !match(line, /\[info\]/i)) continue;
if (match(line, /tachyon|sing-box|watchdog/i)) push(filtered, line);
}
let start = length(filtered) - count;
if (start < 0) start = 0;
for (let i = start; i < length(filtered); i++) {
text += escape_html(filtered[i]) + "\n";
}
if (length(filtered) == 0) text += "Нет записей для данного уровня.\n";
} else {
text += "Не удалось прочитать логи.\n";
}
text += "</pre>";
let keyboard = [
[
{ text: "❌ Errors", callback_data: "/logs error 30" },
{ text: "⚠️ Warns", callback_data: "/logs warn 30" }
],
[
{ text: "ℹ️ Info", callback_data: "/logs info 30" },
{ text: "📋 All", callback_data: "/logs all 30" }
],
[{ text: "🔄 Обновить", callback_data: "/logs " + level + " " + as_string(count) }],
[{ text: "⬅️ Назад", 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);
}
function view_system_info(token, chat_id, msg_id) {
let text = "ℹ️ <b>Информация о системе</b>\n\n";
let res = command_capture(command_from_args(["/usr/lib/tachyon/diagnostics/runtime.uc", "get-system-info"]));
if (res && res.status == 0 && res.output) {
try {
let info = json(res.output);
text += "📱 Устройство: <code>" + as_string(info.device_model || "N/A") + "</code>\n";
text += "💻 OpenWrt: <code>" + as_string(info.openwrt_version || "N/A") + "</code>\n\n";
text += "🔧 Tachyon: <code>" + as_string(info.tachyon_version || "N/A") + "</code>\n";
text += "📱 LuCI App: <code>" + as_string(info.luci_app_version || "N/A") + "</code>\n\n";
text += "📦 sing-box: <code>" + as_string(info.sing_box_version || "N/A") + "</code>\n";
if (info.sing_box_extended) text += " Extended: ✅\n";
if (info.sing_box_tiny) text += " Tiny: ✅\n";
if (info.sing_box_tailscale) text += " Tailscale: ✅\n";
text += "\n";
if (info.zapret_installed == "1") text += "🛡 Zapret: <code>" + as_string(info.zapret_version || "?") + "</code>\n";
if (info.zapret2_installed == "1") text += "🛡 Zapret2: <code>" + as_string(info.zapret2_version || "?") + "</code>\n";
if (info.byedpi_installed == "1") text += "🛡 ByeDPI: <code>" + as_string(info.byedpi_version || "?") + "</code>\n";
} catch(e) {
text += "Ошибка чтения информации: " + e;
}
} else {
text += "❌ Не удалось получить информацию о системе.";
}
let keyboard = [
[{ text: "🔄 Обновить", callback_data: "/info" }],
[{ text: "⬅️ Назад", 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);
}
// ─── Phase 3: Connections, Help ────────────────────────────────────────────
function view_connections(token, chat_id, msg_id, page) {
page = int(page || 0);
let data = api.get_clash_connections();
let text = "🔗 <b>Активные подключения</b>\n\n";
if (!data || !data.connections || length(data.connections) == 0) {
text += "Нет активных подключений.";
let keyboard = [
[{ text: "🔄 Обновить", callback_data: "/connections" }],
[{ text: "⬅️ Назад", 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";
let per_page = 8;
let total = length(data.connections);
let start = page * per_page;
let end = start + per_page;
if (end > total) end = total;
for (let i = start; i < end; i++) {
let c = data.connections[i];
let metadata = c.metadata || {};
let proto = metadata.type || "?";
let dest = metadata.destinationIP || "?";
let local = metadata.sourceIP || "?";
let chain = "";
if (c.chain && length(c.chain) > 0) chain = c.chain[length(c.chain) - 1];
let dl = format_bytes(c.download);
let ul = format_bytes(c.upload);
text += "• <code>" + escape_html(proto) + "</code> " + escape_html(local) + " → " + escape_html(dest) + "\n";
text += " ↳ " + escape_html(chain) + " 📥" + dl + " 📤" + ul + "\n";
}
let total_pages = (total + per_page - 1) / per_page;
let keyboard = [];
if (total_pages > 1) {
let nav = [];
if (page > 0) push(nav, { text: "◀️ Prev", callback_data: "/connections " + as_string(page - 1) });
push(nav, { text: as_string(page + 1) + "/" + as_string(int(total_pages)), callback_data: "/noop" });
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: "⬅️ Назад", 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);
}
function view_help(token, chat_id, msg_id) {
let text = "📖 <b>Справка по командам</b>\n\n" +
"<b>Основные:</b>\n" +
"/status — Статус системы\n" +
"/runtime — Статистика трафика\n" +
"/outbounds — Прокси серверы\n" +
"/sections — Секции маршрутизации\n" +
"/devices — Устройства в сети\n" +
"/instances — Live серверы\n\n" +
"<b>Диагностика:</b>\n" +
"/speed — Тест скорости\n" +
"/ping — Задержка до серверов\n" +
"/test — Быстрая диагностика\n" +
"/doctor — Полная диагностика\n" +
"/logs — Просмотр логов\n" +
"/info — Информация о системе\n" +
"/connections — Активные подключения\n\n" +
"<b>Управление:</b>\n" +
"/restart — Перезапуск роутера\n" +
"/backup — Бэкап конфига\n" +
"/close_connections — Закрыть все соединения\n" +
"/check_updates — Проверить обновления\n\n" +
"<b>Настройка:</b>\n" +
"/settings — Все настройки\n" +
"/dns_presets — DNS пресеты\n" +
"/help — Эта справка";
let keyboard = [[{ text: "⬅️ Меню", 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);
}
// ─── Phase 4: Quiet Hours, Rule Test, Export ───────────────────────────────
function view_quiet_hours(token, chat_id, msg_id) {
let c = uci_core.cursor();
c.load(CONFIG_NAME);
let s = c.get_all(CONFIG_NAME, "telegram");
let enabled = option(s, "quiet_hours_enabled", "0") == "1";
let start_hr = option(s, "quiet_hours_start", "23");
let end_hr = option(s, "quiet_hours_end", "7");
let text = "🔕 <b>Тихие часы</b>\n\n" +
"Статус: " + (enabled ? "🟢 Включены" : "❌ Выключены") + "\n" +
"Начало: <code>" + start_hr + ":00</code>\n" +
"Конец: <code>" + end_hr + ":00</code>\n\n" +
"В тихие часы бот отправляет только критические уведомления.";
let keyboard = [
[{ text: (enabled ? "❌ Выключить" : "✅ Включить"), callback_data: "/qh_toggle" }],
[{ text: "⏰ Начало: " + start_hr, callback_data: "/qh_start" }],
[{ text: "⏰ Конец: " + end_hr, callback_data: "/qh_end" }],
[{ text: "⬅️ Назад", 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_qh_toggle(token, chat_id, msg_id) {
let c = uci_core.cursor();
c.load(CONFIG_NAME);
let s = c.get_all(CONFIG_NAME, "telegram");
let current = option(s, "quiet_hours_enabled", "0");
c.set(CONFIG_NAME, "telegram", "quiet_hours_enabled", current == "1" ? "0" : "1");
c.commit(CONFIG_NAME);
return view_quiet_hours(token, chat_id, msg_id);
}
function view_test_rule(token, chat_id, msg_id, target) {
if (!target) {
set_tg_state(chat_id, { action: "test_rule" });
return send_message(token, chat_id, "🔍 Введите домен или IP для проверки:\n\n<i>Отправьте /cancel для отмены</i>", "HTML");
}
let text = "🔍 <b>Проверка правила:</b> <code>" + escape_html(target) + "</code>\n\n";
let sections = api.get_sections();
let matched = false;
for (let s in sections) {
let sec = sections[s];
if (sec[".type"] == "settings" || sec[".type"] == "telegram") continue;
if (sec.enabled == "0") continue;
let domain_suffix = common.list_option(sec, "domain_suffix");
let domain = common.list_option(sec, "domain");
let ip_cidr = common.list_option(sec, "ip_cidr");
for (let d in domain_suffix) {
if (match(target, escape_html(d) + "$") || target == d) {
text += "✅ Совпадение: <code>" + escape_html(sec[".name"]) + "</code> (domain_suffix)\n";
text += " Действие: <code>" + as_string(sec.action || "proxy") + "</code>\n\n";
matched = true;
}
}
for (let d in domain) {
if (target == d || target == "full:" + d) {
text += "✅ Совпадение: <code>" + escape_html(sec[".name"]) + "</code> (domain)\n";
text += " Действие: <code>" + as_string(sec.action || "proxy") + "</code>\n\n";
matched = true;
}
}
for (let ip in ip_cidr) {
if (match(target, /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) && match(ip, /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) {
text += "✅ Совпадение: <code>" + escape_html(sec[".name"]) + "</code> (ip_cidr)\n";
text += " Действие: <code>" + as_string(sec.action || "proxy") + "</code>\n\n";
matched = true;
}
}
}
if (!matched) text += "❌ Совпадений не найдено. Трафик пойдёт через default route.";
let keyboard = [
[{ text: "🔄 Ещё раз", callback_data: "/test_rule" }],
[{ text: "⬅️ Назад", 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);
}
function exec_export_config(token, chat_id, msg_id) {
let wait_text = "📤 <b>Экспорт конфига...</b>\n";
if (msg_id) edit_message(token, chat_id, msg_id, wait_text, "HTML");
else send_message(token, chat_id, wait_text, "HTML");
let export_path = "/tmp/tachyon_export_" + time() + ".json";
let res = command_capture(command_from_args(["/usr/lib/tachyon/diagnostics/runtime.uc", "show-config"]));
if (res && res.status == 0 && res.output) {
fs.writefile(export_path, res.output);
send_document(token, chat_id, export_path);
try { fs.unlink(export_path); } catch(e) {}
send_message(token, chat_id, "✅ Конфиг экспортирован.\n\n⚠️ Файл содержит чувствительные данные. Не публикуйте его.", "HTML", [[{ text: "⬅️ Меню", callback_data: "/menu" }]]);
} else {
send_message(token, chat_id, "❌ Не удалось экспортировать конфиг.", "HTML", [[{ text: "⬅️ Меню", callback_data: "/menu" }]]);
}
}
function view_devices(token, chat_id, msg_id) { function view_devices(token, chat_id, msg_id) {
let lease_file = "/tmp/dhcp.leases"; let lease_file = "/tmp/dhcp.leases";
let data = fs.readfile(lease_file); let data = fs.readfile(lease_file);
@@ -1322,6 +1714,29 @@ function dispatch_command(token, chat_id, text, msg_id) {
if (cmd == "/heal" || cmd == "/ai_heal") return exec_ai_heal(token, chat_id, msg_id); if (cmd == "/heal" || cmd == "/ai_heal") return exec_ai_heal(token, chat_id, msg_id);
if (cmd == "/qos") return view_qos(token, chat_id, msg_id); if (cmd == "/qos") return view_qos(token, chat_id, msg_id);
if (cmd == "/qos_toggle") return handle_qos_toggle(token, chat_id, msg_id); if (cmd == "/qos_toggle") return handle_qos_toggle(token, chat_id, msg_id);
if (cmd == "/speed") return exec_speedtest(token, chat_id, msg_id);
if (cmd == "/ping") return view_ping(token, chat_id, 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 (match(cmd, /^\/logs /)) {
let parts = split(trim(substr(cmd, 6)), " ");
return view_logs(token, chat_id, msg_id, parts[0] || "all", parts[1] || "30");
}
if (cmd == "/logs") return view_logs(token, chat_id, msg_id);
if (match(cmd, /^\/connections /)) {
let page = trim(substr(cmd, 13));
return view_connections(token, chat_id, msg_id, page);
}
if (cmd == "/connections") return view_connections(token, chat_id, msg_id);
if (cmd == "/test_rule") return view_test_rule(token, chat_id, msg_id);
if (cmd == "/export_config") return exec_export_config(token, chat_id, msg_id);
if (cmd == "/qh") return view_quiet_hours(token, chat_id, msg_id);
if (cmd == "/qh_toggle") return handle_qh_toggle(token, chat_id, msg_id);
if (cmd == "/outbounds") return view_outbounds(token, chat_id, msg_id); if (cmd == "/outbounds") return view_outbounds(token, chat_id, msg_id);
if (match(cmd, /^\/outbounds /)) { if (match(cmd, /^\/outbounds /)) {
@@ -1795,6 +2210,9 @@ function process_updates(token, admin_ids) {
} }
view_sec_list(token, chat_id, null, state.sec, state.list); view_sec_list(token, chat_id, null, state.sec, state.list);
} }
else if (state.action == "test_rule") {
view_test_rule(token, chat_id, null, trim(msg.text));
}
continue; continue;
} }
@@ -1871,16 +2289,24 @@ function worker() {
if (cfg.enabled != "1" || !cfg.bot_token) return 0; if (cfg.enabled != "1" || !cfg.bot_token) return 0;
let commands = [ let commands = [
{ command: "menu", description: "Main Menu" }, { command: "menu", description: "Главное меню" },
{ command: "status", description: "System status" }, { command: "status", description: "Статус системы" },
{ command: "runtime", description: "Runtime stats" }, { command: "runtime", description: "Статистика трафика" },
{ command: "outbounds", description: "Proxy servers" }, { command: "outbounds", description: "Прокси серверы" },
{ command: "sections", description: "Routing sections" }, { command: "sections", description: "Секции маршрутизации" },
{ command: "instances", description: "Live Server Instances" }, { command: "instances", description: "Live серверы" },
{ command: "check_updates", description: "Check component updates" }, { command: "speed", description: "Тест скорости" },
{ command: "close_connections", description: "Close All Connections" }, { command: "ping", description: "Задержка до серверов" },
{ command: "doctor", description: "Diagnostics" }, { command: "test", description: "Быстрая диагностика" },
{ command: "restart", description: "Restart router" } { 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: "Перезапуск роутера" }
]; ];
tg_request(cfg.bot_token, "setMyCommands", { commands: commands }); tg_request(cfg.bot_token, "setMyCommands", { commands: commands });