1502 lines
63 KiB
Bash
1502 lines
63 KiB
Bash
#!/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'
|
||
|
||
clear
|
||
|
||
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() {
|
||
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
|