#!/bin/sh SCRIPT_VERSION="v0.2.0-alpha" MIHOMO_INSTALL_DIR="/etc/mihomo" MIHOMO_BIN="/usr/bin/mihomo" GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' GH_RAW_HOST="https://raw.githubusercontent.com"; GH_MAIN_HOST="https://github.com"; GH_PROXY="https://gh-proxy.org/"; GH_CHECK_URL="${GH_RAW_HOST}/StressOzz/Zapret-Manager/refs/heads/main/Zapret-Manager.sh" echo -e "${CYAN}Проверяем доступность ${NC}raw.githubusercontent.com"; if wget -q -T 2 -O /dev/null "$GH_CHECK_URL" 2>/dev/null; then GH_OK=1; GH_RAW="$GH_RAW_HOST" GH_MAIN="$GH_MAIN_HOST"; echo -e "raw.githubusercontent.com ${GREEN}доступен!${NC}\n"; else GH_OK=0; GH_RAW="${GH_PROXY}${GH_RAW_HOST}"; GH_MAIN="${GH_PROXY}${GH_MAIN_HOST}" echo -e "raw.githubusercontent.com ${RED}недоступен${CYAN} — ${YELLOW}используем прокси!${NC}\n"; fi log_online() { echo -e "${GREEN}[ONLINE]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } log_done() { echo -e "${GREEN}$*${NC}"; } step_fail() { echo -e "${RED}[FAIL]${NC}"; exit 1; } USE_APK=0 if command -v apk > /dev/null 2>&1; then USE_APK=1 fi manage_pkg() { local action="$1" shift if [ "$USE_APK" -eq 1 ]; then case "$action" in update) apk update ;; install) apk add "$@" ;; remove) apk del "$@" ;; esac else case "$action" in update) opkg update ;; install) opkg install "$@" ;; remove) opkg remove "$@" ;; esac fi } detect_mihomo_arch() { local arch arch=$(uname -m) local endian_byte endian_byte=$(hexdump -s 5 -n 1 -e '1/1 "%d"' /bin/busybox 2>/dev/null || echo "0") case "$arch" in x86_64) if grep -q "avx2" /proc/cpuinfo; then echo "amd64" else echo "amd64-compatible" fi ;; i?86) echo "386" ;; aarch64|arm64) echo "arm64" ;; armv7*) echo "armv7" ;; armv5*|armv4*) echo "armv5" ;; mips*) local fpu fpu=$(grep -c "FPU" /proc/cpuinfo 2>/dev/null || echo 0) local floattype="softfloat" [ "$fpu" -gt 0 ] && floattype="hardfloat" if [ "$endian_byte" = "1" ]; then echo "mipsle-${floattype}" else echo "mips-${floattype}" fi ;; riscv64) echo "riscv64" ;; *) log_error "Архитектура $arch не распознана" exit 1 ;; esac } verify_required_deps() { local missing=0 if ! command -v curl >/dev/null 2>&1; then log_error "Пакет curl не найден!" missing=1 fi if [ ! -f /etc/ssl/certs/ca-certificates.crt ] && [ ! -f /etc/ssl/certs/ca-bundle.crt ]; then log_error "Пакет ca-certificates не найден!" missing=1 fi if [ ! -c /dev/net/tun ]; then modprobe tun >/dev/null 2>&1 || true if [ ! -c /dev/net/tun ]; then log_error "В ядре нет поддержки TUN (/dev/net/tun)!" missing=1 fi fi if [ "$missing" -eq 1 ]; then return 1 fi return 0 } install_deps() { log_online "Установка зависимостей" local PKG_LOG="/tmp/install_deps.log" if [ "$USE_APK" -eq 1 ]; then apk update > "$PKG_LOG" 2>&1 || true local AVAIL_PKG AVAIL_PKG=$(grep -o '[0-9]* distinct packages available' "$PKG_LOG" | grep -o '^[0-9]*') if [ -z "$AVAIL_PKG" ] || [ "$AVAIL_PKG" -eq 0 ]; then log_warn "apk update не вернул доступных пакетов, повторная попытка..." sleep 3 apk update > "$PKG_LOG" 2>&1 || true AVAIL_PKG=$(grep -o '[0-9]* distinct packages available' "$PKG_LOG" | grep -o '^[0-9]*') if [ -z "$AVAIL_PKG" ] || [ "$AVAIL_PKG" -eq 0 ]; then log_error "apk update завершился без доступных пакетов:" cat "$PKG_LOG" rm -f "$PKG_LOG" return 1 fi fi apk add unzip ca-certificates kmod-tun kmod-nft-tproxy kmod-nft-nat curl >> "$PKG_LOG" 2>&1 || true else if ! opkg update > "$PKG_LOG" 2>&1; then log_error "Ошибка обновления списков пакетов (opkg update):" cat "$PKG_LOG" rm -f "$PKG_LOG" return 1 fi opkg install unzip ca-certificates kmod-tun kmod-nft-tproxy kmod-nft-nat curl libcurl4 ca-bundle >> "$PKG_LOG" 2>&1 || true fi rm -f "$PKG_LOG" if ! verify_required_deps; then log_error "Не удалось подтвердить наличие обязательных компонентов!" return 1 fi } install_mihomo() { local REQ_TMP_KB=16000 local REQ_ROOT_KB=18000 local AVAIL_TMP_KB AVAIL_TMP_KB=$(df -k /tmp | awk 'NR==2 {print $4}') if [ "$AVAIL_TMP_KB" -lt "$REQ_TMP_KB" ]; then log_error "Недостаточно места в /tmp: доступно $((AVAIL_TMP_KB/1024)) MB, требуется $((REQ_TMP_KB/1024)) MB" return 1 fi local INSTALL_DIR_PATH INSTALL_DIR_PATH=$(dirname "$MIHOMO_BIN") local AVAIL_ROOT_KB AVAIL_ROOT_KB=$(df -k "$INSTALL_DIR_PATH" | awk 'NR==2 {print $4}') if [ "$AVAIL_ROOT_KB" -lt "$REQ_ROOT_KB" ]; then log_error "Недостаточно места на диске: доступно $((AVAIL_ROOT_KB/1024)) MB, требуется $((REQ_ROOT_KB/1024)) MB" if [ -f "$MIHOMO_BIN" ]; then log_warn "Найдена установленная версия: $MIHOMO_BIN" printf "Удалить старую версию для освобождения места? [y/+/д или n/-/н]: " read -r response case "$response" in [yY+дД]*) rm -f "$MIHOMO_BIN" AVAIL_ROOT_KB=$(df -k "$INSTALL_DIR_PATH" | awk 'NR==2 {print $4}') if [ "$AVAIL_ROOT_KB" -lt "$REQ_ROOT_KB" ]; then log_error "Места всё равно недостаточно после удаления." return 1 fi ;; *) log_warn "Установка отменена." return 1 ;; esac else log_warn "Старая версия не найдена. Удалите лишние пакеты вручную." return 1 fi fi if [ -f "/etc/init.d/mihomo" ]; then /etc/init.d/mihomo stop 2>/dev/null || true fi if [ -z "${MIHOMO_ARCH+x}" ]; then MIHOMO_ARCH=$(detect_mihomo_arch) fi echo "Архитектура системы: $(uname -m) -> выбран файл: $MIHOMO_ARCH" mkdir -p "$MIHOMO_INSTALL_DIR" \ /etc/mihomo/proxy-providers \ /etc/mihomo/rule-providers \ /etc/mihomo/rule-files echo "$MIHOMO_ARCH" > /etc/mihomo/.arch echo "Получение номера последней версии" local RELEASE_TAG RELEASE_TAG=$(curl -Ls -o /dev/null -w '%{url_effective}' https://github.com/MetaCubeX/mihomo/releases/latest | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1) if [ -z "$RELEASE_TAG" ]; then log_error "Не удалось определить версию. Проверьте интернет." return 1 fi echo "Последняя версия: $RELEASE_TAG" local FILENAME="mihomo-linux-${MIHOMO_ARCH}-${RELEASE_TAG}.gz" local DOWNLOAD_URL="${GH_MAIN}/MetaCubeX/mihomo/releases/download/${RELEASE_TAG}/${FILENAME}" local TMP_FILE="/tmp/mihomo.gz" log_online "Скачивание архива $FILENAME" log_online "$DOWNLOAD_URL" if ! curl -Lf --retry 3 --retry-delay 2 "$DOWNLOAD_URL" -o "$TMP_FILE" >/dev/null 2>&1; then log_error "Ошибка скачивания! Проверьте, существует ли файл $FILENAME в релизах." return 1 fi echo "Распаковка архива" if ! gunzip -c "$TMP_FILE" > "$MIHOMO_BIN" 2>/dev/null; then log_error "Ошибка распаковки архива" rm -f "$TMP_FILE" return 1 fi chmod +x "$MIHOMO_BIN" rm -f "$TMP_FILE" echo "Проверка работы ядра Mihomo" if ! "$MIHOMO_BIN" -v >/dev/null 2>&1; then log_error "Ядро не запускается! Возможно, выбрана неверная архитектура." return 1 fi local CONFIG_FILE="/etc/mihomo/config.yaml" local WRITE_NEW_CONFIG=1 if [ -f "$CONFIG_FILE" ]; then if grep -q "mixed-port: 7890" "$CONFIG_FILE"; then echo "Использование существующей конфигурации" WRITE_NEW_CONFIG=0 else log_warn "Конфигурация найдена, но без 'mixed-port: 7890'. Создаём резервную копию" cp "$CONFIG_FILE" "${CONFIG_FILE}.bak" fi fi if [ "$WRITE_NEW_CONFIG" -eq 1 ]; then echo "Создание новой конфигурации /etc/mihomo/config.yaml..." cat > "$CONFIG_FILE" <<'EOF' mode: rule ipv6: false mixed-port: 7890 log-level: error allow-lan: true unified-delay: true tcp-concurrent: false find-process-mode: off external-controller: 0.0.0.0:9090 external-ui: ./ui routing-mark: 2 profile: store-selected: true store-fake-ip: true tracing: true sniffer: enable: true force-dns-mapping: true parse-pure-ip: true sniff: HTTP: ports: [80] override-destination: true TLS: ports: [443, 8443] QUIC: ports: [443, 8443] skip-domain: - Mijia Cloud - +.lan - +.local - +.msftconnecttest.com - +.msftncsi.com - +.3gppnetwork.org - +.openwrt.org - +.vsean.net - cudy.net dns: enable: true listen: 0.0.0.0:7880 ipv6: false nameserver: - https://8.8.8.8/dns-query - https://8.8.4.4/dns-query - https://1.1.1.1/dns-query - https://1.0.0.1/dns-query - https://9.9.9.9/dns-query - https://149.112.112.112/dns-query - https://94.140.14.140/dns-query - https://94.140.14.141/dns-query - https://77.88.8.8/dns-query - https://77.88.8.1/dns-query proxies: - name: Домашний интернет type: direct proxy-groups: rule-providers: rules: - MATCH,Домашний интернет EOF fi echo "Создание службы /etc/init.d/mihomo" cat > /etc/init.d/mihomo <<'EOF' #!/bin/sh /etc/rc.common START=99 USE_PROCD=1 MIHOMO_BIN="/usr/bin/mihomo" MIHOMO_DIR="/etc/mihomo" MIHOMO_CONF="/etc/mihomo/config.yaml" start_service() { [ -x "$MIHOMO_BIN" ] || return 1 [ -s "$MIHOMO_CONF" ] || return 1 procd_open_instance "main" procd_set_param command "$MIHOMO_BIN" -d "$MIHOMO_DIR" -f "$MIHOMO_CONF" procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param respawn procd_close_instance } service_triggers() { procd_add_reload_trigger "mihomo" } EOF chmod +x /etc/init.d/mihomo /etc/init.d/mihomo enable || log_warn "Не удалось включить автозапуск" echo "Настройка страницы LuCI для управления Mihomo" mkdir -p /usr/share/luci/menu.d cat > /usr/share/luci/menu.d/luci-app-mihomo.json <<'EOF' { "admin/services/mihomo": { "title": "Mihomo", "order": 60, "action": { "type": "view", "path": "mihomo/config" }, "depends": { "acl": [ "luci-app-mihomo" ] } } } EOF mkdir -p /usr/share/rpcd/acl.d cat > /usr/share/rpcd/acl.d/luci-app-mihomo.json <<'EOF' { "luci-app-mihomo": { "description": "Mihomo control", "read": { "file": { "/etc/mihomo/config.yaml": ["read"], "/etc/mihomo/rule-files/": ["list"], "/etc/mihomo/rule-files/*": ["read"] }, "ubus": { "file": ["read", "list"], "service": ["list"] } }, "write": { "file": { "/etc/mihomo/config.yaml": ["write"], "/etc/mihomo/rule-files/*": ["write"], "/usr/bin/mihomo": ["exec"], "/etc/init.d/mihomo": ["exec"], "/sbin/logread": ["exec"], "/bin/sh": ["exec"], "/bin/ash": ["exec"], "/usr/bin/curl": ["exec"], "/usr/bin/wget": ["exec"], "/bin/gzip": ["exec"], "/bin/chmod": ["exec"], "/bin/mv": ["exec"], "/bin/rm": ["exec"] }, "ubus": { "file": ["write"], "service": ["list"] } } } } EOF local VIEW_PATH="/www/luci-static/resources/view/mihomo" local ACE_PATH="$VIEW_PATH/ace" mkdir -p "$ACE_PATH" echo "Определение актуальной версии ACE Editor" local LATEST_ACE_VER LATEST_ACE_VER=$(curl -s "https://api.cdnjs.com/libraries/ace" | grep -o '"version":"[^"]*"' | cut -d'"' -f4 | head -1) if [ -z "$LATEST_ACE_VER" ]; then log_warn "cdnjs API недоступен, пробуем GitHub API" LATEST_ACE_VER=$(curl -s "https://api.github.com/repos/ajaxorg/ace/releases/latest" | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4 | sed 's/^v//' | head -1) fi if [ -z "$LATEST_ACE_VER" ]; then log_warn "Используем фиксированную версию ACE Editor" LATEST_ACE_VER="1.43.3" else echo "Актуальная версия ACE Editor: $LATEST_ACE_VER" fi log_online "Скачивание файлов для ACE Editor $LATEST_ACE_VER:" local CDNJS_ACE_VER="1.43.6" for file in ace.js theme-merbivore_soft.js theme-tomorrow.js mode-yaml.js worker-yaml.js; do local dest="${ACE_PATH}/${file}" local success=0 for url in "https://cdn.jsdelivr.net/npm/ace-builds@${LATEST_ACE_VER}/src-min-noconflict/${file}" \ "${GH_RAW}/ajaxorg/ace-builds/master/src-min-noconflict/${file}" \ "https://cdnjs.cloudflare.com/ajax/libs/ace/${CDNJS_ACE_VER}/${file}"; do log_online "Скачивание $file" if curl -Lf -s --connect-timeout 5 --max-time 30 -o "$dest" "$url" || wget -q -T 5 -O "$dest" "$url"; then if [ -s "$dest" ]; then success=1 break fi fi echo "FAIL" done if [ "$success" -eq 0 ]; then log_error "Не удалось скачать $file ни из одного источника." return 1 fi done echo "Создание config.js" cat > "$VIEW_PATH/config.js" <<'EOF' 'use strict'; 'require view'; 'require fs'; 'require ui'; 'require rpc'; var ACE_DIR = '/luci-static/resources/view/mihomo/ace/'; var RELOAD_DELAY = 1000; var MAIN_CONFIG = '/etc/mihomo/config.yaml'; var RULE_DIR = '/etc/mihomo/rule-files/'; var editor = null; var currentFile = MAIN_CONFIG; var cachedRuleFiles = []; var mainConfigContent = ''; var loadedScripts = {}; var VALID_ACTIONS = ['start', 'stop', 'restart', 'check', 'logs']; var callServiceList = rpc.declare({ object: 'service', method: 'list', params: ['name'] }); function escapeHtml(text) { if (typeof text !== 'string') return text; return text.replace(/[&<>"']/g, function(m) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]; }); } function validatePath(path, allowedBase) { if (!path || typeof path !== 'string') return false; if (path.includes('..') || path.includes('\0') || path.includes('~')) return false; var resolved = path.replace(/\/+/g, '/'); if (!resolved.startsWith(allowedBase)) return false; if (resolved.length > 1024) return false; return true; } function isSafeRulePath(path) { return validatePath(path, RULE_DIR) && path !== MAIN_CONFIG; } function validateFilename(filename) { if (!filename || typeof filename !== 'string') return false; if (!/^[a-zA-Z0-9._-]+$/.test(filename)) return false; if (filename.length > 255) return false; var reservedNames = ['con', 'prn', 'aux', 'nul', 'com1', 'lpt1', '.']; if (reservedNames.includes(filename.toLowerCase())) return false; return true; } function sanitizeTabName(name) { if (!name) return ''; return name.replace(/[<>"'`]/g, ''); } function loadScript(src) { return new Promise(function(resolve, reject) { if (loadedScripts[src]) { resolve(); return; } var script = document.createElement('script'); script.src = src; script.onload = function() { loadedScripts[src] = true; resolve(); }; script.onerror = reject; document.head.appendChild(script); }); } function detectRuleType(line) { line = line.trim(); if (line.includes(':') && !line.match(/http(s)?:\/\//)) return 'IP-CIDR6'; if (/^\d{1,3}(\.\d{1,3}){3}\/\d+$/.test(line)) return 'IP-CIDR'; if (/^\d{1,3}(\.\d{1,3}){3}$/.test(line)) return 'IP-CIDR'; if (line.startsWith('.')) return 'DOMAIN-WILDCARD'; var cleanDomain = line.replace(/^\./, ''); var dots = (cleanDomain.match(/\./g) || []).length; if (dots >= 2) return 'DOMAIN'; if (dots === 1) return 'DOMAIN-SUFFIX'; return 'DOMAIN-KEYWORD'; } function generateProviderSnippet(filename) { if (filename === MAIN_CONFIG) return ''; var baseName = filename.split('/').pop(); if (!validateFilename(baseName)) throw new Error('Invalid filename'); var nameNoExt = baseName.replace(/\.(yaml|txt)$/, ''); var isTxt = baseName.endsWith('.txt'); var behavior = isTxt ? 'domain' : 'classical'; var format = isTxt ? 'text' : 'yaml'; return `${nameNoExt}-list:\n type: file\n behavior: ${behavior}\n format: ${format}\n path: ./rule-files/${baseName}`; } function isLuciDarkMode() { try { var rgb = window.getComputedStyle(document.body).backgroundColor.match(/\d+/g); if (rgb) { var luma = 0.2126 * parseInt(rgb[0]) + 0.7152 * parseInt(rgb[1]) + 0.0722 * parseInt(rgb[2]); return luma < 128; } } catch(e) {} return false; } return view.extend({ isProcessing: false, currentVersion: 'Неизвестно', latestVersion: null, updateButton: null, latestVersionEl: null, getMihomoVersion: function() { return fs.stat('/usr/bin/mihomo') .then(function() { return fs.exec('/usr/bin/mihomo', ['--v']); }) .then(function(res) { if (res.code === 0 && res.stdout) { var match = res.stdout.match(/v(\d+\.\d+\.\d+)/); return match ? match[0] : 'Неизвестно'; } return 'Неизвестно'; }) .catch(function(err) { console.error('Error getting version:', err); return 'Неизвестно'; }); }, renderUpdateStatus: function(latestVersion, isManual) { var currentVersion = this.currentVersion || 'Неизвестно'; this.latestVersion = latestVersion; if (this.latestVersionEl) { this.latestVersionEl.textContent = _('(актуальное ядро %s)').format(latestVersion.replace('v', '')); this.latestVersionEl.style.display = 'inline'; // Используем стандартный зеленый через opacity/filter или оставляем для привлечения внимания this.latestVersionEl.style.color = (latestVersion !== currentVersion) ? '#5cb85c' : ''; this.latestVersionEl.style.opacity = (latestVersion !== currentVersion) ? '1' : '0.6'; } if (latestVersion === currentVersion) { this.updateButton.textContent = _('Проверить обновление'); this.updateButton.className = 'btn cbi-button-neutral'; this.updateButton.disabled = false; this.updateButton.onclick = function() { window.location.reload(); }; if (isManual) window.location.reload(); } else { this.updateButton.textContent = _('Установить обновление'); this.updateButton.className = 'btn cbi-button-action'; this.updateButton.disabled = false; this.updateButton.onclick = ui.createHandlerFn(this, 'handleUpdateMihomo'); } }, checkForUpdates: function(isManual) { var self = this; var CACHE_KEY = 'mihomo_update_cache'; var CACHE_TIME = 3600 * 1000; if (!isManual) { try { var cachedRaw = localStorage.getItem(CACHE_KEY); if (cachedRaw) { var cached = JSON.parse(cachedRaw); if (cached.version && (Date.now() - cached.timestamp < CACHE_TIME)) { this.renderUpdateStatus(cached.version, false); return; } } } catch (e) {} } if (isManual) ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Проверка обновлений...'))]); var cmd = 'wget -q -O - "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" 2>/dev/null | grep -m1 \'"tag_name":\' | sed \'s/.*"\\(v[0-9.]*\\)".*/\\1/\''; fs.exec('/bin/sh', ['-c', cmd]) .then(function(res) { if (isManual) ui.hideModal(); if (!res || typeof res !== 'object') throw new Error('Bad response'); var latestVersion = (res.stdout || '').trim().replace(/["'\s]/g, ''); if (!latestVersion || !latestVersion.match(/^v\d+\.\d+\.\d+$/)) { if (isManual) ui.addNotification(null, E('p', _('Ошибка: ') + latestVersion), 'error'); return; } try { localStorage.setItem(CACHE_KEY, JSON.stringify({ version: latestVersion, timestamp: Date.now() })); } catch (e) {} self.renderUpdateStatus(latestVersion, isManual); }) .catch(function(err) { if (isManual) { ui.hideModal(); ui.addNotification(null, E('p', _('Ошибка: ') + err.message), 'error'); } }); }, handleUpdateMihomo: function() { var self = this; var latestVersion = this.latestVersion; if (!latestVersion) return; this.updateButton.textContent = _('Подождите...'); this.updateButton.disabled = true; var arch = 'arm64'; var downloadUrl = 'https://github.com/MetaCubeX/mihomo/releases/download/' + latestVersion + '/mihomo-linux-' + arch + '-' + latestVersion + '.gz'; var steps = [ { msg: _('Создание бэкапа...'), shell: 'cp -f /usr/bin/mihomo /tmp/mihomo.backup' }, { msg: _('Остановка Mihomo...'), shell: '/etc/init.d/mihomo stop' }, { msg: _('Скачивание архива %s...').format(latestVersion), shell: 'wget -q -O /tmp/mihomo.gz "' + downloadUrl + '" && test -s /tmp/mihomo.gz' }, { msg: _('Распаковка архива...'), shell: '/bin/gzip -d -c /tmp/mihomo.gz > /tmp/mihomo_new 2>/dev/null && test -s /tmp/mihomo_new' }, { msg: _('Выдача временных прав...'), shell: '/bin/chmod 755 /tmp/mihomo_new' }, { msg: _('Проверка ядра...'), shell: '/tmp/mihomo_new -v 2>&1 || true' }, { msg: _('Установка ядра...'), shell: '/bin/mv -f /tmp/mihomo_new /usr/bin/mihomo' }, { msg: _('Выдача постоянных прав...'), shell: '/bin/chmod 755 /usr/bin/mihomo' }, { msg: _('Запуск Mihomo...'), shell: '/etc/init.d/mihomo start' }, { msg: _('Удаление бэкапа...'), shell: 'rm -f /tmp/mihomo.gz /tmp/mihomo.backup' } ]; var executeStep = function(index) { if (index >= steps.length) { self.showOutput(_('Обновлено успешно! Перезагрузка...'), false); window.location.reload(); return Promise.resolve(); } var currentStep = steps[index]; self.showOutput(currentStep.msg, false); return fs.exec('/bin/sh', ['-c', currentStep.shell]) .then(function(res) { if (!res || res.code !== 0) throw new Error('Err: ' + (res ? res.code : 'unknown')); return executeStep(index + 1); }); }; executeStep(0).catch(function(err) { if (self.updateButton) { self.updateButton.textContent = _('Ошибка. Повторить обновление?'); self.updateButton.disabled = false; self.updateButton.onclick = ui.createHandlerFn(self, 'handleUpdateMihomo'); } self.showOutput(_('Ошибка: %s').format(err.message), true); fs.exec('/bin/sh', ['-c', 'cp -f /tmp/mihomo.backup /usr/bin/mihomo && /etc/init.d/mihomo start']).catch(function() {}); }); }, load: function() { return Promise.all([ fs.read(MAIN_CONFIG).catch(function() { return ''; }), callServiceList('mihomo').catch(function() { return {}; }), fs.list(RULE_DIR).catch(function() { return []; }) ]); }, render: function(data) { data = data || []; mainConfigContent = data[0] || ''; var serviceInfo = data[1] || {}; cachedRuleFiles = (data[2] || []).sort(function(a, b) { return a.name.localeCompare(b.name); }); var isRunning = !!(serviceInfo.mihomo && serviceInfo.mihomo.instances.main.running); var versionContainer = E('span', { 'id': 'mihomo-version', 'style': 'margin-left: 10px; font-size: 0.9em; opacity: 0.7;' }, _('Загрузка...')); var latestVersionEl = E('span', { 'id': 'mihomo-latest-version', 'style': 'margin-left: 4px; font-size: 0.9em; opacity: 0.7; display: none;' }, ''); this.latestVersionEl = latestVersionEl; var updateButton = E('button', { 'id': 'mihomo-update-btn', 'class': 'btn cbi-button-neutral', 'style': 'margin-left: 10px; padding: 0 0.6em; font-size: 0.9em;', 'disabled': true }, _('Проверить обновление')); this.updateButton = updateButton; var statusBadge = isRunning ? E('span', { 'class': 'label success', 'style': 'margin-left: 14px; font-size: 0.85em; min-height: 1.7rem; padding: 0 1.9em; display: inline-flex; align-items: center; vertical-align: middle;' }, _('работает')) : E('span', { 'class': 'label', 'style': 'margin-left: 14px; font-size: 0.85em; min-height: 1.7rem; padding: 0 1.9em; display: inline-flex; align-items: center; vertical-align: middle;' }, _('остановлен')); var serviceButton = isRunning ? E('button', { 'class': 'btn cbi-button-reset', 'style': 'margin-left: 16px;', 'click': ui.createHandlerFn(this, 'handleServiceAction', 'stop') }, _('Остановить')) : E('button', { 'class': 'btn cbi-button-positive btn-save-custom', 'style': 'margin-left: 16px;', 'click': ui.createHandlerFn(this, 'handleServiceAction', 'start') }, _('Запустить')); var header = E('div', { 'style': 'display: flex; align-items: center; margin-bottom: 1rem; flex-wrap: wrap;' }, [ E('h2', { 'style': 'margin: 0;' }, _('Mihomo')), statusBadge, serviceButton, versionContainer, latestVersionEl, updateButton ]); var self = this; this.getMihomoVersion().then(function(version) { self.currentVersion = version; var versionEl = document.getElementById('mihomo-version'); if (versionEl) versionEl.textContent = _('%s').format(version.replace('v', '')); var updateBtn = document.getElementById('mihomo-update-btn'); if (updateBtn) { updateBtn.disabled = false; updateBtn.onclick = function() { self.checkForUpdates(true); }; } self.checkForUpdates(false); }); var isDark = isLuciDarkMode(); var cssVariables = isDark ? ` :root { --bg-tab: #2d2d2d; --bg-tab-active: #1C1C1C; --bg-toolbar: #1C1C1C; --bg-input: #2d2d2d; --text-main: #e0e0e0; --text-dim: #969696; --border-color: #444444; --border-active: #444444; --bg-output: #222222; --bg-output-header: #333333; --text-output: #f8f8f2; } ` : ` :root { --bg-tab: #e0e0e0; --bg-tab-active: #ffffff; --bg-toolbar: #f5f5f5; --bg-input: #ffffff; --text-main: #333333; --text-dim: #666666; --border-color: #E0E0E0; --border-active: #E0E0E0; --bg-output: #ffffff; --bg-output-header: #eeeeee; --text-output: #333333; } `; var style = E('style', {}, cssVariables + ` .btn, .cbi-button { min-height: 1.8rem !important; display: inline-flex !important; align-items: center; justify-content: center; vertical-align: middle; box-sizing: border-box !important; /* Чтобы padding не раздувал кнопку */ padding: 0 1rem !important; line-height: 1 !important; } #output-text { font-size: 0.8rem !important; } .cbi-page-actions { display: none !important; } .custom-actions { display: flex; gap: 0.5rem; } .tab-bar { display: flex; flex-wrap: nowrap; background-color: var(--bg-tab); } .tab-item { display: flex; align-items: center; padding: 0.6em 1.2em; cursor: pointer; background-color: var(--bg-tab); color: var(--text-dim); margin-right: 1px; font-size: 0.9em; border-top: 1px solid transparent; white-space: nowrap; user-select: none; box-sizing: border-box } .tab-item:hover { background-color: var(--bg-toolbar); color: var(--text-main); } .tab-item.active { background-color: var(--bg-tab-active); color: var(--text-main); border: 1px solid var(--border-active); } .tab-close { margin-left: 0.6em; border-radius: 3px; padding: 0 0.3em; color: #999; font-weight: bold; } .tab-close:hover { background-color: #c0392b; color: white; } .tab-new { font-weight: bold; font-size: 1.2em; padding: 0.5em 0.8em; } .toolbar { background-color: var(--bg-toolbar); border: 1px solid var(--border-color); padding: 0.8rem; color: var(--text-main); } .toolbar-row { display: flex; gap: 0.8rem; align-items: center; } .toolbar textarea { width: 100%; height: 6em; background: var(--bg-input); color: var(--text-main); border: 1px solid var(--border-color); font-family: monospace; font-size: 0.9em; padding: 0.4em; } .toolbar select { background: var(--bg-input); color: var(--text-main); border: 1px solid var(--border-color); padding: 0.4em; } .toolbar-col { display: flex; flex-direction: column; } .btn-save-custom { border-color: #5cb85c !important; color: #5cb85c !important; } .btn-save-custom:hover { border-color: #5cb85c !important; } .btn.cbi-button-action:hover { border-color: #5cb85c !important; } .btn.cbi-button-reset:hover { border-color: #F62B12 !important; color: #F62B12 !important; } .btn-generate { border-color: #5cb85c !important; color: #5cb85c !important; margin: auto 0; display: block; background: var(--bg-input); } .btn-generate:hover { border-color: #5cb85c !important; } .snippet-container { margin-top: 0; border: 1px solid var(--border-color); background: var(--bg-toolbar); padding: 0.8rem; display: none; } .snippet-header { margin-bottom: 0.4rem; color: var(--text-main); font-size: 0.85em; } .snippet-text { width: 100%; height: 9.5em; background: var(--bg-tab-active); color: var(--text-main); border: 1px solid var(--border-color); font-family: monospace; font-size: 0.9em; padding: 0.8em; resize: none; } .output-box-close { background: transparent; border: none; color: var(--text-main); font-size: 1.5em; line-height: 1; cursor: pointer; margin-left: 1rem; padding: 0 0.4rem; } .output-box-close:hover { color: #e74c3c !important; } #ace_editor_container { width: 100%; height: 60vh; border: 1px solid var(--border-color); border-top: none; } `); var tabBar = E('div', { 'id': 'mihomo-tab-bar', 'class': 'tab-bar' }); var toolbarContainer = E('div', { 'id': 'mihomo-toolbar' }); var editorContainer = E('div', { 'id': 'ace_editor_container' }); var snippetContainer = E('div', { 'id': 'snippet-box', 'class': 'snippet-container', 'style': 'margin-top: 0.8rem' }, [ E('div', { 'class': 'snippet-header', 'style': 'opacity: 0.7' }, _('Чтобы Mihomo увидел файл, добавьте эту секцию в rule-providers:')), E('textarea', { 'id': 'snippet-area', 'class': 'snippet-text', 'readonly': 'readonly', 'style': 'opacity: 0.8' }), E('div', { 'style': 'margin-top: 0.8rem; display: flex; gap: 0.6rem;' }, [ E('button', { 'class': 'btn cbi-button-apply', 'click': ui.createHandlerFn(this, 'handleAutoAddSnippet') }, _('Добавить автоматически')), E('button', { 'class': 'btn cbi-button-neutral', 'click': ui.createHandlerFn(this, 'handleCopySnippet') }, _('Скопировать текст')) ]) ]); var buttonContainer = E('div', { 'id': 'bottom-buttons', 'class': 'custom-actions', 'style': 'margin-top: 1rem;' }, [ E('button', { 'class': 'btn cbi-button-neutral', 'click': ui.createHandlerFn(this, 'handleCheck') }, _('Проверить конфигурацию')), E('button', { 'class': 'btn cbi-button-positive btn-save-custom', 'click': ui.createHandlerFn(this, 'handleSaveAndApply', isRunning) }, _('Сохранить')), E('button', { 'class': 'btn cbi-button-neutral', 'click': ui.createHandlerFn(this, 'handleOpenDashboard', mainConfigContent) }, _('Открыть панель управления')), E('button', { 'class': 'btn cbi-button-neutral', 'click': ui.createHandlerFn(this, 'handleShowLogs') }, _('Показать журнал')) ]); var middleActions = E('div', { 'id': 'middle-actions', 'style': 'display: none; margin-top: 0.8rem;' }, [ E('button', { 'class': 'btn cbi-button-positive btn-save-custom', 'click': ui.createHandlerFn(this, 'handleSaveAndApply', isRunning) }, _('Сохранить')) ]); var outputBox = E('div', { 'id': 'output-box', 'style': 'display: none; margin-top: 1.2rem; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden;' }, [ E('div', { 'style': 'background: var(--bg-output-header); color: var(--text-output); padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--border-color); display: flex; align-items: center;' }, [ E('strong', { 'style': 'font-size: 0.9em' }, _('Вывод:')), E('button', { 'class': 'output-box-close', 'click': function() { document.getElementById('output-box').style.display = 'none'; } }, '×') ]), E('pre', { 'id': 'output-text', 'style': 'margin: 0; padding: 1rem; background: var(--bg-output); color: var(--text-output); font-family: monospace; font-size: 1em; white-space: pre-wrap; word-wrap: break-word; max-height: 25rem; overflow-y: auto;' }, '') ]); loadScript(ACE_DIR + 'ace.js').then(function() { ace.config.set('basePath', ACE_DIR); editor = ace.edit("ace_editor_container"); var theme = isDark ? "ace/theme/merbivore_soft" : "ace/theme/tomorrow"; editor.setTheme(theme); editor.session.setMode("ace/mode/yaml"); editor.setOptions({ fontSize: "0.95em", showPrintMargin: false, wrap: true, tabSize: 2, useSoftTabs: true, highlightActiveLine: false }); editor.setValue(mainConfigContent, -1); setTimeout(function() { editor.resize(); }, 100); }).catch(console.error); this.renderTabBar(tabBar); this.renderToolbar(toolbarContainer, MAIN_CONFIG); setTimeout(function() { this.updateVisibility(MAIN_CONFIG); }.bind(this), 100); return E('div', { 'class': 'cbi-map' }, [ header, style, tabBar, toolbarContainer, editorContainer, middleActions, snippetContainer, buttonContainer, outputBox ]); }, updateVisibility: function(filePath) { var isMain = (filePath === MAIN_CONFIG); document.getElementById('bottom-buttons').style.display = isMain ? 'flex' : 'none'; document.getElementById('middle-actions').style.display = isMain ? 'none' : 'block'; var snippetBox = document.getElementById('snippet-box'); if (isMain) { snippetBox.style.display = 'none'; } else { var baseName = filePath.split('/').pop().replace(/\.(yaml|txt)$/, ''); var providerName = baseName + '-list:'; if (mainConfigContent.includes(providerName)) { snippetBox.style.display = 'none'; } else { document.getElementById('snippet-area').value = generateProviderSnippet(filePath); snippetBox.style.display = 'block'; } } }, handleAutoAddSnippet: function() { var self = this; var snippet = generateProviderSnippet(currentFile); if (!snippet) return; ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Добавление...'))]); var indentedSnippet = snippet.split('\n').map(function(line) { return ' ' + line; }).join('\n'); fs.read(MAIN_CONFIG).then(function(content) { var newContent = content || ''; var sectionMatch = newContent.match(/^rule-providers:\s*$/m); if (!sectionMatch) { var proxiesMatch = newContent.match(/^proxies:\s*$/m); var pgMatch = newContent.match(/^proxy-groups:\s*$/m); var lastIdx = Math.max(proxiesMatch ? proxiesMatch.index + proxiesMatch[0].length : -1, pgMatch ? pgMatch.index + pgMatch[0].length : -1); if (lastIdx > -1) { var textAfter = newContent.substring(lastIdx); var nextMatch = textAfter.match(/\n(?![ \t])[a-z][^:\n]*:/i); if (nextMatch) { var insIdx = lastIdx + nextMatch.index; newContent = newContent.substring(0, insIdx) + '\nrule-providers:\n\n' + indentedSnippet + '\n' + newContent.substring(insIdx); } else { newContent = newContent.trimEnd() + '\n\nrule-providers:\n\n' + indentedSnippet + '\n'; } } else { if (newContent && !newContent.endsWith('\n')) newContent += '\n'; newContent += '\nrule-providers:\n\n' + indentedSnippet + '\n'; } } else { var secEnd = sectionMatch.index + sectionMatch[0].length; var textAfter = newContent.substring(secEnd); var nextMatch = textAfter.match(/\n(?![ \t])[a-z][^:\n]*:/i); if (nextMatch) { var insIdx = secEnd + nextMatch.index; newContent = newContent.substring(0, insIdx) + '\n' + indentedSnippet + '\n' + newContent.substring(insIdx); } else { newContent = newContent.substring(0, secEnd).replace(/\n+$/, '\n') + '\n' + indentedSnippet + '\n'; } } mainConfigContent = newContent; return fs.write(MAIN_CONFIG, newContent); }).then(function() { self.updateVisibility(currentFile); ui.hideModal(); }).catch(function(err) { ui.hideModal(); ui.addNotification(null, E('p', _('Ошибка: ') + err.message), 'error'); }); }, handleCopySnippet: function() { var area = document.getElementById('snippet-area'); if (area) { area.select(); document.execCommand('copy'); } }, renderToolbar: function(container, filePath) { L.dom.content(container, []); if (filePath === MAIN_CONFIG) { container.style.display = 'none'; return; } container.style.display = 'block'; container.className = 'toolbar'; var self = this; if (filePath.endsWith('.txt')) { var input = E('textarea', { 'placeholder': 'google.com\nyoutube.com' }); var suffixCheck = E('input', { 'type': 'checkbox', 'id': 'suffixCheck', 'checked': true }); var row = E('div', { 'class': 'toolbar-row' }, [ E('div', { 'style': 'flex-grow: 1;' }, input), E('div', { 'class': 'toolbar-col', 'style': 'min-width: 10rem; display: flex; flex-direction: column; justify-content: space-between;' }, [ E('label', { 'for': 'suffixCheck', 'style': 'align-self: flex-start; font-size: 0.85em;' }, [ suffixCheck, ' . (дубликаты с точкой)' ]), E('button', { 'class': 'btn btn-generate', 'style': 'align-self: center;', 'click': function() { self.handleAppendList(input.value, suffixCheck.checked); input.value = ''; } }, _('Добавить')) ]) ]); container.appendChild(row); } else { var input = E('textarea', { 'placeholder': 'google.com\n104.28.0.0/16\n*.example.com' }); var typeSelect = E('select', { 'style': 'font-size: 0.9em' }, [ E('option', { 'value': 'Auto' }, 'Auto'), E('option', { 'value': 'DOMAIN-SUFFIX' }, 'DOMAIN-SUFFIX'), E('option', { 'value': 'DOMAIN' }, 'DOMAIN'), E('option', { 'value': 'DOMAIN-KEYWORD' }, 'DOMAIN-KEYWORD'), E('option', { 'value': 'DOMAIN-WILDCARD' }, 'DOMAIN-WILDCARD'), E('option', { 'value': 'IP-CIDR' }, 'IP-CIDR'), E('option', { 'value': 'IP-CIDR6' }, 'IP-CIDR6') ]); var row = E('div', { 'class': 'toolbar-row' }, [ E('div', { 'style': 'flex-grow: 1;' }, input), E('div', { 'class': 'toolbar-col' }, [ typeSelect ]), E('div', { 'class': 'toolbar-col', 'style': 'min-width: 8rem; justify-content: flex-end;' }, [ E('button', { 'class': 'btn btn-generate', 'click': function() { self.handleGenerateRules(input.value, typeSelect.value); input.value = ''; } }, _('Создать')) ]) ]); container.appendChild(row); } }, handleAppendList: function(text, addSuffix) { if (!editor || !text.trim()) return; var lines = text.trim().split('\n'); var result = []; lines.forEach(function(line) { line = line.trim(); if (!line) return; result.push(line); if (addSuffix && !line.startsWith('.')) result.push('.' + line); }); if (result.length > 0) { editor.navigateFileEnd(); var doc = editor.getValue(); var prefix = (doc.length > 0 && !doc.endsWith('\n')) ? '\n' : ''; editor.insert(prefix + result.join('\n') + '\n'); editor.focus(); } }, handleGenerateRules: function(text, type) { if (!editor || !text.trim()) return; var lines = text.trim().split('\n'); var newRules = []; lines.forEach(function(line) { line = line.trim(); if (!line) return; var currentType = type === 'Auto' ? detectRuleType(line) : type; if (currentType === 'IP-CIDR' && !line.includes('/')) line += '/32'; newRules.push(` - ${currentType},${line}`); }); if (newRules.length === 0) return; var content = editor.getValue(); var linesContent = content.split('\n'); var payloadIndex = linesContent.findIndex(function(l) { return l.trim() === 'payload:'; }); if (payloadIndex !== -1) { editor.gotoLine(linesContent.length + 1, 0); editor.insert(newRules.join('\n') + '\n'); } else { var prefix = (content.length > 0 && !content.endsWith('\n')) ? '\n\n' : ''; editor.navigateFileEnd(); editor.insert(prefix + 'payload:\n' + newRules.join('\n') + '\n'); } editor.focus(); }, renderTabBar: function(container) { L.dom.content(container, []); var self = this; var mainTab = E('div', { 'class': (currentFile === MAIN_CONFIG) ? 'tab-item active' : 'tab-item', 'click': ui.createHandlerFn(this, 'handleTabClick', MAIN_CONFIG) }, E('span', {}, 'Конфигурация')); container.appendChild(mainTab); cachedRuleFiles.forEach(function(file) { if (file.type === 'file') { var fullPath = RULE_DIR + file.name; if (!validatePath(fullPath, RULE_DIR)) return; var isActive = (currentFile === fullPath); var safeName = escapeHtml(sanitizeTabName(file.name)); var tabContent = [E('span', {}, safeName)]; if (isActive) { tabContent.push(E('span', { 'class': 'tab-close', 'title': _('Удалить'), 'click': ui.createHandlerFn(self, 'handleDeleteFile', fullPath) }, '×')); } var tab = E('div', { 'class': isActive ? 'tab-item active' : 'tab-item', 'click': ui.createHandlerFn(self, 'handleTabClick', fullPath) }, tabContent); container.appendChild(tab); } }); var newTab = E('div', { 'class': 'tab-item tab-new', 'title': _('Создать новый'), 'click': ui.createHandlerFn(this, 'handleCreateFile') }, '+'); container.appendChild(newTab); }, handleTabClick: function(path, ev) { if (!validatePath(path, '/etc/mihomo/')) { ui.addNotification(null, E('p', _('Недопустимый путь')), 'error'); return; } if (ev && ev.target.classList.contains('tab-close')) { ev.stopPropagation(); return; } if (path === currentFile) return; var self = this; ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Загрузка...'))]); fs.read(path).then(function(content) { currentFile = path; if (editor) { editor.setValue(content || '', -1); editor.session.setMode(path.endsWith('.txt') ? "ace/mode/text" : "ace/mode/yaml"); } self.renderTabBar(document.getElementById('mihomo-tab-bar')); self.renderToolbar(document.getElementById('mihomo-toolbar'), path); self.updateVisibility(path); ui.hideModal(); }).catch(function(err) { if (err && err.message === 'Данные не получены') { currentFile = path; if (editor) { editor.setValue('', -1); editor.session.setMode(path.endsWith('.txt') ? "ace/mode/text" : "ace/mode/yaml"); } self.renderTabBar(document.getElementById('mihomo-tab-bar')); self.renderToolbar(document.getElementById('mihomo-toolbar'), path); self.updateVisibility(path); } else { ui.addNotification(null, E('p', _('Ошибка: ') + (err.message || 'Error')), 'error'); } ui.hideModal(); }); }, handleCreateFile: function() { var self = this; var nameInput = E('input', { 'type': 'text', 'style': 'width: 100%;', 'placeholder': 'my-rules' }); var typeSelect = E('select', { 'style': 'width: 100%;' }, [ E('option', { 'value': '.yaml' }, 'Набор правил (.yaml)'), E('option', { 'value': '.txt' }, 'Простой список (.txt)') ]); var footer = E('div', { 'class': 'right', 'style': 'margin-top: 1.5rem;' }, [ E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Отмена')), ' ', E('button', { 'class': 'btn cbi-button-positive btn-save-custom', 'click': function() { var filename = nameInput.value.trim(); if (!filename || !validateFilename(filename)) { ui.addNotification(null, E('p', _('Некорректное имя')), 'error'); return; } var fullPath = RULE_DIR + filename + typeSelect.value; if (!validatePath(fullPath, RULE_DIR)) { ui.addNotification(null, E('p', _('Недопустимый путь')), 'error'); return; } ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Создание...'))]); fs.stat(fullPath).then(function() { ui.hideModal(); ui.addNotification(null, E('p', _('Файл уже существует')), 'error'); }).catch(function() { fs.write(fullPath, '').then(function() { return fs.list(RULE_DIR); }).then(function(files) { cachedRuleFiles = (files || []).sort(function(a, b) { return a.name.localeCompare(b.name); }); self.handleTabClick(fullPath); }).catch(function(err) { ui.hideModal(); ui.addNotification(null, E('p', _('Ошибка: ') + err.message), 'error'); }); }); }}, _('Создать')) ]); ui.showModal(_('Новый файл правил'), [ E('div', {}, [ E('div', { 'style': 'display: flex; align-items: center; margin-bottom: 0.8rem;' }, [ E('label', { 'style': 'min-width: 10rem; margin-right: 0.8rem;' }, _('Имя файла:')), nameInput ]), E('div', { 'style': 'display: flex; align-items: center;' }, [ E('label', { 'style': 'min-width: 10rem; margin-right: 0.8rem;' }, _('Тип файла:')), typeSelect ]) ]), footer ]); nameInput.focus(); }, handleDeleteFile: function(path) { if (!isSafeRulePath(path)) { ui.addNotification(null, E('p', _('Ошибка пути')), 'error'); return; } if (!confirm(_('Удалить %s?').format(path.split('/').pop()))) return; var self = this; ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Удаление...'))]); fs.remove(path).then(function() { return fs.list(RULE_DIR); }).then(function(files) { cachedRuleFiles = (files || []).sort(function(a, b) { return a.name.localeCompare(b.name); }); if (currentFile === path) self.handleTabClick(MAIN_CONFIG); else { self.renderTabBar(document.getElementById('mihomo-tab-bar')); ui.hideModal(); } }).catch(function(err) { ui.hideModal(); ui.addNotification(null, E('p', _('Ошибка: ') + err.message), 'error'); }); }, handleSaveAndApply: function(wasRunning) { if (this.isProcessing) return Promise.reject(new Error('Busy')); if (!editor) return; this.isProcessing = true; var self = this; var content = editor.getValue(); if (currentFile === MAIN_CONFIG) mainConfigContent = content; ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Сохранение...'))]); fs.write(currentFile, content).then(function() { if (currentFile === MAIN_CONFIG) { return fs.exec('/usr/bin/mihomo', ['-d', '/etc/mihomo', '-t', MAIN_CONFIG]).then(function(res) { if (res.code !== 0) throw new Error((res.stdout || '') + (res.stderr || '')); if (wasRunning) return fs.exec('/etc/init.d/mihomo', ['restart']); }); } }).then(function() { ui.hideModal(); if (currentFile === MAIN_CONFIG) setTimeout(function() { window.location.reload(); }, RELOAD_DELAY); }).catch(function(err) { self.showOutput(err.message, true); ui.hideModal(); }).finally(function() { self.isProcessing = false; }); }, handleCheck: function() { if (currentFile !== MAIN_CONFIG || !editor) return; var self = this; ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Проверка...'))]); fs.write(MAIN_CONFIG, editor.getValue()) .then(function() { return fs.exec('/usr/bin/mihomo', ['-d', '/etc/mihomo', '-t']); }) .then(function(res) { self.showOutput((res.stdout || '') + (res.stderr || ''), res.code !== 0); ui.hideModal(); }) .catch(function(e) { self.showOutput(e.message, true); ui.hideModal(); }); }, showOutput: function(text, isError) { var box = document.getElementById('output-box'); var out = document.getElementById('output-text'); if (box && out) { out.textContent = text ? text.trim() : '(Пусто)'; out.style.color = isError ? '#f92672' : 'var(--text-output)'; box.style.display = 'block'; box.scrollIntoView({ behavior: 'smooth', block: 'end' }); } }, handleServiceAction: function(act) { if (!VALID_ACTIONS.includes(act)) return; var self = this; ui.showModal(null, [E('p', { 'class': 'spinning' }, _('Выполнение...'))]); fs.exec('/etc/init.d/mihomo', [act]).then(function() { window.location.reload(); }) .catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); }); }, handleShowLogs: function() { var self = this; fs.exec('/sbin/logread', ['-e', 'mihomo']).then(function(res) { var logContent = res.stdout; if (!logContent && res.code !== 0) { logContent = "Записей о 'mihomo' в системном журнале не найдено.\nВозможно, служба не запущена."; } else if (!logContent) { logContent = "Журнал пуст."; } self.showOutput(logContent, false); }).catch(function(err) { self.showOutput("Ошибка чтения журнала: " + err.message, true); }); }, handleOpenDashboard: function(content) { var hostname = window.location.hostname; var port = '9090'; try { var match = content.match(/external-controller:\s*([0-9\.]+):(\d+)/); if (match && match[1] && match[2]) { var extractedIp = match[1].trim(); if (/^(\d{1,3}\.){3}\d{1,3}$/.test(extractedIp) && extractedIp !== '0.0.0.0') hostname = extractedIp; var portNum = parseInt(match[2].trim(), 10); if (!isNaN(portNum) && portNum >= 1 && portNum <= 65535) port = match[2].trim(); } } catch (e) {} window.open(`http://${hostname}:${port}/ui/`, '_blank'); } }); EOF } install_hev_tunnel() { log_online "Установка hev-socks5-tunnel" if [ "$USE_APK" -eq 1 ]; then apk cache clean apk add hev-socks5-tunnel >/dev/null 2>&1 else manage_pkg install hev-socks5-tunnel >/dev/null 2>&1 fi rm -f /etc/hev-socks5-tunnel/main.yml mkdir -p /etc/hev-socks5-tunnel cat > /etc/hev-socks5-tunnel/main.yml <<'EOF' tunnel: name: Mihomo mtu: 8500 multi-queue: false ipv4: 198.18.0.1 socks5: port: 7890 address: 127.0.0.1 udp: 'udp' EOF chmod 600 /etc/hev-socks5-tunnel/main.yml echo "Очистка старых настроек UCI" uci delete network.Mihomo 2>/dev/null || true local fw_section for fw_section in $(uci show firewall 2>/dev/null \ | grep -E "\.name='Mihomo'" \ | sed "s/\.name.*//"); do uci delete "$fw_section" 2>/dev/null || true done for fw_section in $(uci show firewall 2>/dev/null \ | grep -E "\.(src|dest)='Mihomo'" \ | sed -E "s/\.(src|dest).*//"); do uci delete "$fw_section" 2>/dev/null || true done uci delete firewall.Mihomo 2>/dev/null || true uci delete firewall.lan_to_Mihomo 2>/dev/null || true uci commit firewall /etc/init.d/firewall restart 2>/dev/null || true sleep 1 echo "Настройка UCI-сервиса hev-socks5-tunnel" if ! uci -q get hev-socks5-tunnel.@instance[0] >/dev/null 2>&1; then uci add hev-socks5-tunnel instance >/dev/null fi uci set hev-socks5-tunnel.@instance[0].enabled='1' uci set hev-socks5-tunnel.@instance[0].conffile='/etc/hev-socks5-tunnel/main.yml' uci commit hev-socks5-tunnel /etc/init.d/hev-socks5-tunnel restart sleep 2 echo "Настройка сетевого интерфейса" uci set network.Mihomo=interface uci set network.Mihomo.proto='none' uci set network.Mihomo.device='Mihomo' uci commit network /etc/init.d/network reload echo "Настройка firewall" local FW_ZONE FW_ZONE=$(uci add firewall zone) uci set "firewall.${FW_ZONE}.name=Mihomo" uci set "firewall.${FW_ZONE}.input=REJECT" uci set "firewall.${FW_ZONE}.output=REJECT" uci set "firewall.${FW_ZONE}.forward=REJECT" uci set "firewall.${FW_ZONE}.masq=1" uci set "firewall.${FW_ZONE}.mtu_fix=1" uci add_list "firewall.${FW_ZONE}.network=Mihomo" local FW_FWD FW_FWD=$(uci add firewall forwarding) uci set "firewall.${FW_FWD}.src=lan" uci set "firewall.${FW_FWD}.dest=Mihomo" uci commit firewall /etc/init.d/firewall restart } install_magitrickle() { ################################################################################################### PAUSE() { echo -ne "Нажмите Enter..."; read dummy; } INSTALL="opkg install"; RAZ="ipk"; SUF="" command -v apk >/dev/null 2>&1 && INSTALL="apk add --allow-untrusted" && RAZ="apk" && SUF="r" ARCH_MT=$(grep "^OPENWRT_ARCH=" /etc/os-release | cut -d'"' -f2) MT_VERSION="$(curl -Ls -o /dev/null -w '%{url_effective}' https://github.com/MagiTrickle/MagiTrickle/releases/latest | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" # MT_VERSION="0.7.0" URL="${GH_MAIN}/MagiTrickle/MagiTrickle/releases/download/${MT_VERSION}/magitrickle_${MT_VERSION}-${SUF}1_openwrt_${ARCH_MT}.$RAZ" FILE_MT="/tmp/$(basename "$URL")"; echo -e "Скачиваем и устанавливаем:\n${CYAN}$URL${NC}" curl -Lf --retry 3 --retry-delay 2 -o "$FILE_MT" "$URL" >/dev/null 2>&1 || { echo -e "\n${RED}Ошибка скачивания${NC}\n"; PAUSE; exit 1; } $INSTALL "$FILE_MT" >/dev/null 2>&1 || { echo -e "\n${RED}Ошибка установки${NC}\n"; PAUSE; rm -f "$FILE_MT"; exit 1; } rm -f "$FILE_MT" ################################################################################################### echo "Создание страницы MagiTrickle в LuCI" mkdir -p /www/luci-static/resources/view/magitrickle cat > /www/luci-static/resources/view/magitrickle/magitrickle.js <<'EOF' 'use strict'; 'require view'; return view.extend({ handleSave: null, handleSaveApply: null, handleReset: null, render: function() { var hostname = window.location.hostname; var port = '8080'; var url = 'http://' + hostname + ':' + port; if (window.location.protocol === 'https:') { return E('div', { style: 'padding: 20px; text-align: center;' }, [ E('p', _('HTTPS соединение блокирует встроенный интерфейс MagiTrickle через LuCI.')), E('p', { style: 'margin-bottom: 20px;' }, _('Пожалуйста, откройте MagiTrickle в новой вкладке для полноценного управления.')), E('a', { 'class': 'btn cbi-button-action', 'href': url, 'target': '_blank', 'style': 'padding: 10px 20px; font-size: 1.1em;' }, _('Открыть MagiTrickle')) ]); } return E('div', { style: 'width:100%; height:92vh; margin: -20px -20px 0 -20px; overflow: hidden;' }, E('iframe', { src: url, style: 'width:100%; height:100%; border: none;' })); } }); EOF cat > /usr/share/luci/menu.d/luci-app-magitrickle.json <<'EOF' { "admin/services/magitrickle": { "title": "MagiTrickle", "order": 60, "action": { "type": "view", "path": "magitrickle/magitrickle" } } } EOF rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/ } finalize_install() { echo "Выставление прав доступа" chmod -R 755 /www/luci-static/resources/view/mihomo 2>/dev/null || true find /www/luci-static/resources/view/mihomo -type f -exec chmod 644 {} \; 2>/dev/null || true chmod 644 /www/luci-static/resources/view/magitrickle/magitrickle.js 2>/dev/null || true echo "Очистка кэша LuCI и перезапуск сервисов" rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/ /etc/init.d/rpcd restart > /dev/null 2>&1 /etc/init.d/uhttpd restart > /dev/null 2>&1 /etc/init.d/mihomo restart > /dev/null 2>&1 } main() { clear log_done "=== Mixomo OpenWrt от Internet Helper (StressOzz Remix) ===" echo "" log_done "[1/5] Установка зависимостей" install_deps || step_fail echo "" log_done "[2/5] Установка Mihomo" install_mihomo || step_fail echo "" log_done "[3/5] Установка Hev-Socks5-Tunnel" install_hev_tunnel || step_fail echo "" log_done "[4/5] Установка MagiTrickle" install_magitrickle || step_fail ################################################################################################### CONFIG_PATH="/etc/magitrickle/state/config.yaml" confGIT="${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/files/MagiTrickle/configAD.yaml" echo "Установка списка для MagiTrickle" for i in 1 2 3; do curl -fL --connect-timeout 3 --max-time 7 -o "$CONFIG_PATH" "$confGIT" >/dev/null 2>&1 && break [ "$i" -eq 3 ] && { echo -e "\n${RED}Не удалось скачать список!${NC}\n" } sleep 1 done /etc/init.d/magitrickle enable >/dev/null 2>&1 /etc/init.d/magitrickle reload >/dev/null 2>&1 /etc/init.d/magitrickle start >/dev/null 2>&1 /etc/init.d/magitrickle restart >/dev/null 2>&1 ################################################################################################### echo "" log_done "[5/5] Завершение" finalize_install || step_fail ################################################################################################### TMP1="/tmp/zashboard.zip" TMP2="/tmp/zashboard" DIR1="/etc/mihomo/ui" URL1="${GH_MAIN}/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip" echo "Устанавка панели Zashboard для Mihomo" for i in 1 2 3; do curl -fL --connect-timeout 3 --max-time 7 -o "$TMP1" "$URL1" >/dev/null 2>&1 && break [ "$i" -eq 3 ] && { echo -e "\n${RED}Ошибка скачивания WEB UI!${NC}\n" rm -f "$TMP1" } sleep 1 done rm -rf "$TMP2" mkdir -p "$TMP2" unzip -oq "$TMP1" -d "$TMP2" || { echo -e "\n${RED}Ошибка распаковки WEB UI!${NC}\n" rm -rf "$TMP1" "$TMP2" } rm -rf "$DIR1" mkdir -p "$DIR1" cp -r /tmp/zashboard/dist/* "$DIR1"/ 2>/dev/null || \ cp -r /tmp/zashboard/* "$DIR1"/ 2>/dev/null || { echo -e "\n${RED}Не удалось установить WEB UI!${NC}\n" rm -rf "$TMP1" "$TMP2" } rm -rf "$TMP1" "$TMP2" /etc/init.d/mihomo restart >/dev/null 2>&1 /etc/init.d/hev-socks5-tunnel restart >/dev/null 2>&1 ################################################################################################### echo "" log_done "Установка Mixomo OpenWrt прошла успешно!" echo "" } main