#!/usr/bin/env python3 """3X-UI_KIT (форк hacker07): накатывает наши «швы» на чистый код автора. Это источник правды для форка: единственное место, где описано, ЧТО именно мы правим в файлах автора. Всё, что мы дописываем по существу, живёт в scripts/kit-local.sh — файле, который автор не трогает, поэтому обновления автора его не ломают. Правила: * каждый якорь обязан найтись РОВНО один раз, иначе — стоп (автор изменил код, правку надо пересмотреть руками); * при любой ошибке ничего не пишется наполовину: сначала прогоняй --check; * открытый ключ подписи берётся из tools/fork-signer.pub (закрытая часть — только на рабочей станции владельца, в репозиторий не попадает). Использование: python3 tools/fork_apply.py --check [корень-репозитория] python3 tools/fork_apply.py [корень-репозитория] """ import os import pathlib import re import sys GITEA = os.environ.get("FORK_GITEA_BASE", "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS") SIGNER_FILE = os.environ.get("FORK_SIGNER_PUB", "tools/fork-signer.pub") # README.md форка — НАШ файл целиком: авторское описание из него убрано, чтобы никто не # ставил сервер авторской командой и не оказался на авторском канале обновлений. Шва здесь нет: # при обновлении из авторского проекта README конфликтует и решается в нашу пользу (см. FORK.md). HOOK = """ # --- 3X-UI_KIT (форк hacker07): локальные команды и хуки --- # Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются. # Все команды форка — под «kit local …» (см. kit local help). KIT_LOCAL_LIB="${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}" if [[ -r $KIT_LOCAL_LIB ]]; then . "$KIT_LOCAL_LIB"; kit_local_dispatch "$@"; fi """ def die(msg): print("! " + msg, file=sys.stderr) sys.exit(1) def ok(msg): print("==> " + msg) def count(text, anchor, regex): if regex: return len(re.findall(anchor, text, re.MULTILINE)) return text.count(anchor) def main(): args = [a for a in sys.argv[1:]] check = False if "--check" in args: check = True args.remove("--check") root = pathlib.Path(args[0] if args else ".").resolve() if not (root / "VERSION").exists(): die("%s не похож на репозиторий 3X-UI_autoVPS (нет VERSION)" % root) os.chdir(root) version = (root / "VERSION").read_text(encoding="utf-8").strip() if not re.fullmatch(r"[0-9]+(\.[0-9]+)+", version): die("VERSION=%r: нужен числовой номер вида 1.2.1" % version) signer_path = pathlib.Path(SIGNER_FILE) if not signer_path.exists(): die( "нет %s — сначала создай ключ подписи:\n" " ssh-keygen -t ed25519 -C \"3X-UI KIT fork releases\" -f ~/.ssh/3x-ui-kit-fork/release_key\n" " mkdir -p tools && ssh-keygen -y -f ~/.ssh/3x-ui-kit-fork/release_key > tools/fork-signer.pub" % SIGNER_FILE ) pub = "" for line in signer_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith(("ssh-ed25519 ", "ssh-rsa ")): pub = line break if not pub: die("%s: не нашёл строку открытого ключа (ssh-ed25519 …)" % signer_path) version_re = r'^KIT_VERSION="[0-9.]+"$' edits = [ ( 'scripts/kit.sh', '# kit – управление сервером 3X-UI KIT: пользователи, обновление, резервная копия.\n', False, '# kit – управление сервером 3X-UI_autoVPS: пользователи, обновление, резервная копия.\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', 'kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }\n', False, 'kit_ref_raw() { echo "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$1"; }\n\n# --- 3X-UI_KIT (форк hacker07): локальные команды и хуки ---\n# Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются.\n# Все команды форка — под «kit local …» (см. kit local help).\nKIT_LOCAL_LIB="${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}"\nif [[ -r $KIT_LOCAL_LIB ]]; then . "$KIT_LOCAL_LIB"; kit_local_dispatch "$@"; fi\n\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', 'Description=3X-UI KIT: общий лимит трафика пользователей (сумма по всем записям)\n', False, 'Description=3X-UI_autoVPS: общий лимит трафика пользователей (сумма по всем записям)\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', 'Description=3X-UI KIT: проверка общего лимита трафика раз в 5 минут\n', False, 'Description=3X-UI_autoVPS: проверка общего лимита трафика раз в 5 минут\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' # SHA256:bNjI10eGVGjdFeDJnAHvUFi73rlrXVy6vKFf8lmNyxU (itsnotkubrick, 2026-10-14; прежний ключ SHA256:VDuGgJ8d… утерян, снят)\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEWDFGq2Wno/ADRS2E6UKsgFbUSLvaiPIfRBvSEGJr81 3X-UI KIT releases"\n', False, ' # Наш шов: здесь только ключ форка (hacker07). Авторские ключи сняты намеренно — сервер обязан\n # принимать обновления лишь из нашего канала (git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS).\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)"\n', 'авто-шов: replace, строк 2', ), ( 'scripts/kit.sh', 'update_hint() {\n', False, 'update_hint() { # подсказка про новую версию\n [[ -d /etc/kit ]] || return 0 # kit удалён — подсказывать нечего (раньше тут сыпались ошибки про latest-version)\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' echo "3X-UI KIT $KIT_VERSION"\n', False, ' echo "3X-UI_autoVPS $KIT_VERSION"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null | awk -v v="## v$1" \'\n', False, ' { curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null; curl -fsS -m 5 "$KIT_RAW/FORK-CHANGELOG.md" 2>/dev/null; } | awk -v v="## v$1" \'\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }\n for f in scripts/kit.sh scripts/kit-sub.py; do\n', False, ' grep -qxE "# (3X-UI KIT|3X-UI_autoVPS) $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }\n for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do\n', 'авто-шов: replace, строк 2', ), ( 'scripts/kit.sh', 'Description=kit-sub: подписка с учётом приложения (3X-UI KIT)\n', False, 'Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', 'Description=3X-UI KIT: автообновление kit и kit-sub (только подписанные релизы)\n', False, 'Description=3X-UI_autoVPS: автообновление kit и kit-sub (только подписанные релизы)\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', 'Description=3X-UI KIT: проверка обновлений раз в сутки\n', False, 'Description=3X-UI_autoVPS: проверка обновлений раз в сутки\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI KIT $KIT_VERSION → $latest"; fi\n', False, ' if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI_autoVPS $KIT_VERSION → $latest"; fi\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n', False, ' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n # Форк: ставим наши локальные дополнения из того же подписанного релиза.\n install -d -m 755 /usr/local/lib/kit\n install -m 644 "$tmp/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n', 'авто-шов: insert, строк 1', ), ( 'scripts/kit.sh', ' echo "${G}✓ Готово: 3X-UI KIT $latest.${N} Пользователи, ссылки и подписки не менялись."\n', False, ' echo "${G}✓ Готово: 3X-UI_autoVPS $latest.${N} Пользователи, ссылки и подписки не менялись."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', 'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/ с Name "3X-UI KIT"\n', False, 'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/ с Name "3X-UI_autoVPS"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' grep -q \'"Name":"3X-UI KIT"\' <<<"$v"\n', False, ' grep -q \'"Name":"3X-UI_autoVPS"\' <<<"$v"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' box "3X-UI KIT $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \\\n', False, ' box "3X-UI_autoVPS $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \\\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' echo "${B}Полное удаление 3X-UI KIT${N}"\n', False, ' echo "${B}Полное удаление 3X-UI_autoVPS${N}"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', '${B}3X-UI KIT $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n', False, '${B}3X-UI_autoVPS $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"\n', False, 'KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$KIT_VERSION"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', 'SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com)\n', False, '# Наш шов: добавлены известные сайты (Microsoft, AMD, Intel, NVIDIA) — их можно выбрать вручную.\nSNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com www.microsoft.com www.amd.com www.intel.com www.nvidia.com)\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', " seq 1 254 | xargs -P 8 -I{} bash -c '\n", False, " # Наш шов: 32 потока вместо 8 и таймаут 3 с — скан подсети занимает секунды, а не минуту.\n seq 1 254 | xargs -P 32 -I{} bash -c '\n", 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \\\n | tr "," "\\n" | sed -n "s/^ *DNS://p"\' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true\n', False, ' echo | timeout 3 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \\\n | tr "," "\\n" | sed -n "s/^ *DNS://p"\' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -60 || true\n', 'авто-шов: replace, строк 2', ), ( 'scripts/3x-ui.sh', ' local me a b c base n names found=0\n', False, ' local me a b c base n names out\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' [[ $me =~ ^[0-9]{1,3}(\\.[0-9]{1,3}){3}$ ]] || return 0\n IFS=. read -r a b c _ <<<"$me"\n', False, ' [[ $me =~ ^[0-9]{1,3}(\\.[0-9]{1,3}){3}$ ]] || return 0\n IFS=. read -r a b c _ <<<"$me"\n # Наш шов: имена проверяются параллельно (8 сразу) — раньше по очереди, и это была основная\n # причина «долго сканирует». Функции передаём в подпроцессы через export -f.\n export -f same_net sni_alive sni_trusted sni_quality sni_ok host_ip\n export HOST\n', 'авто-шов: insert, строк 2', ), ( 'scripts/3x-ui.sh', ' names=$(nearby_scan "$base")\n for n in $names; do\n [[ $n == "$me" || $n =~ $SNI_DYN_RE || $n =~ $SNI_BRAND_RE || $n =~ $SNI_BAD_RE ]] && continue\n if same_net "$n" && sni_alive "$n" && sni_trusted "$n" && sni_quality "$n"; then echo "$n"; found=$((found + 1)); ((found >= 8)) && return 0; fi\n done\n ((found > 0)) && return 0\n', False, ' names=$(nearby_scan "$base" | grep -vE "^(${me})$" || true)\n [[ -n $names ]] || continue\n out=$(while IFS= read -r n; do\n [[ $n =~ $SNI_DYN_RE || $n =~ $SNI_BRAND_RE || $n =~ $SNI_BAD_RE ]] && continue\n printf \'%s\\n\' "$n"\n done <<<"$names" | xargs -P 8 -I{} bash -c \'same_net "$1" && sni_alive "$1" && sni_trusted "$1" && sni_quality "$1" && printf "%s\\n" "$1"\' _ {} 2>/dev/null | head -8)\n [[ -n $out ]] && { printf \'%s\\n\' "$out"; return 0; }\n', 'авто-шов: replace, строк 6', ), ( 'scripts/3x-ui.sh', ' [[ -x $acme ]] || { warn "Не нашёл acme.sh, которым установщик 3X-UI получает сертификаты: не могу выпустить сертификат для $DOMAIN."; return 1; }\n', False, ' [[ -x $acme ]] || { warn "Не нашёл acme.sh, которым установщик 3X-UI получает сертификаты: не могу выпустить сертификат для $DOMAIN."; return 1; }\n # Наш шов: лимит Let\'s Encrypt считается на точный набор имён (5 сертификатов за 7 суток). Если у acme.sh\n # уже лежит годный сертификат на этот домен — берём его и не заказываем новый, лишний заказ упирается в лимит.\n local need_issue=yes\n if [[ -s /root/.acme.sh/${DOMAIN}_ecc/fullchain.cer ]] \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkhost "$DOMAIN" 2>/dev/null | grep -q \'does match\' \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkend 86400 >/dev/null 2>&1; then\n ok "у acme.sh уже есть годный сертификат для ${B}$DOMAIN${N} — беру его, новый не заказываю"\n need_issue=no\n fi\n', 'авто-шов: insert, строк 1', ), ( 'scripts/3x-ui.sh', ' [[ $rc == 0 || $rc == 2 ]] || { warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"; return 1; }\n', False, ' [[ $rc == 0 || $rc == 2 ]] || {\n # Наш шов: лимит Let\'s Encrypt — частая причина, и она не про сервер. Говорим прямо и с датой повтора.\n if grep -qE \'rateLimited|too many certificates\' "$log"; then\n local till\n till=$(grep -oE \'retry after [0-9-]+ [0-9:]+ UTC\' "$log" | tail -1 | sed \'s/^retry after //\')\n warn "Лимит Let\'s Encrypt: на имя ${B}$DOMAIN${N} за 7 суток уже выпущено 5 сертификатов${till:+ — повтор возможен после $till}. С вашим сервером всё в порядке: DNS, порты и ufw не при чём. Подождите до этой даты либо возьмите другое имя (например node3.…): лимит считается на точный набор имён."\n else\n warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"\n fi\n return 1\n }\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' local san="IP:$HOST"\n [[ $HOST =~ ^[0-9.]+$ ]] || san="DNS:$HOST"\n', False, ' local san="IP:$HOST"\n [[ $HOST =~ ^[0-9.]+$ ]] || san="DNS:$HOST"\n # Наш шов: если причина — лимит Let\'s Encrypt, говорим это прямо и с датой повтора, как для домена.\n # Иначе человек читает «частые причины» и ищет проблему у хостера, которой нет.\n local till\n till=$(grep -rhoE \'retry after [0-9-]+ [0-9:]+ UTC\' /var/log/3x-ui-install.log "$HOME"/.acme.sh/*/ca 2>/dev/null | tail -1 | sed \'s/^retry after //\')\n if grep -rqsE \'rateLimited|too many certificates\' /var/log/3x-ui-install.log 2>/dev/null; then\n warn "Причина: лимит Let\'s Encrypt — на этот IP за 7 суток уже выпущено 5 сертификатов${till:+; повтор возможен после $till}. Это ограничение Let\'s Encrypt, а не вашего сервера: порт 80 и файрвол тут не при чём."\n else\n', 'авто-шов: insert, строк 2', ), ( 'scripts/3x-ui.sh', ' echo " – сбой у самого Let\'s Encrypt (лог: /var/log/3x-ui-install.log)."\n', False, ' echo " – сбой у самого Let\'s Encrypt (лог: /var/log/3x-ui-install.log)."\n } >&2\n fi\n {\n', 'авто-шов: insert, строк 1', ), ( 'scripts/3x-ui.sh', " _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\\n", False, " _ _ _ ___ _____\n | | | | __ _ ___| | _____ _ __ / _ \\___ |\n | |_| |/ _` |/ __| |/ / _ \\ '__| | | | | / /\n | _ | (_| | (__| < __/ | | |_| |/ /\n |_| |_|\\__,_|\\___|_|\\_\\___|_| \\___//_/\n", 'авто-шов: replace, строк 5', ), ( 'scripts/3x-ui.sh', ' echo "${B}3X-UI KIT $KIT_VERSION${N} · it\'s not Kubrick. it\'s just a VPN."\n', False, ' echo "${B}3X-UI_autoVPS $KIT_VERSION${N} · сборка форка: панель 3X-UI и VPN-протоколы на одном порту"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n', False, ' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n# ---------------------------------------------------------------------------\n# Наш шов: выбор сайта маскировки по результатам скана подсети.\n# Печатает выбранное имя в stdout (пусто — оставляем автоподбор автора).\n# role — подпись для человека: REALITY или XHTTP/MTProto.\nkl_sni_menu() {\n local role=$1; shift\n local -a found=("$@")\n local -a known=("${SNI_CANDIDATES[@]}")\n local src=${KL_ASK_SRC:-/dev/tty} ans i k n auto_idx known_idx own_idx try\n n=${#found[@]}\n printf \'\\n%s%s%s\\n\' "$B" "$role" "$N" >&2\n if ((n)); then\n printf \'Найдено в подсети:\\n\\n\' >&2\n for i in "${!found[@]}"; do printf \' %d - %s\\n\' "$((i + 1))" "${found[i]}" >&2; done\n auto_idx=$((n + 1)); known_idx=$((n + 2)); own_idx=$((n + 3))\n printf \' %d - автовыбор из списка\\n\' "$auto_idx" >&2\n printf \' %d - выбрать общедоступный (например %s, %s)\\n\' "$known_idx" "${known[0]}" "${known[1]}" >&2\n printf \' %d - ввести свой домен\\n\\n\' "$own_idx" >&2\n else\n auto_idx=0; known_idx=1; own_idx=2\n printf \'В подсети доменов не найдено, выберите:\\n\\n\' >&2\n printf \' 1 - общедоступный (например %s, %s)\\n\' "${known[0]}" "${known[1]}" >&2\n printf \' 2 - ввести свой домен\\n\\n\' >&2\n fi\n printf \'Ваш выбор (Enter — автовыбор): \' >&2\n for try in 1 2 3; do\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ -z $ans ]] && return 0\n [[ $ans =~ ^[0-9]+$ ]] || { printf \' введите номер из списка\\n\' >&2; continue; }\n i=$((10#$ans))\n if ((n)) && ((i >= 1 && i <= n)); then printf \'sni:%s\' "${found[i - 1]}"; return 0; fi\n ((i == auto_idx)) && return 0\n if ((i == known_idx)); then\n printf \'\\nОбщедоступные сайты:\\n\' >&2\n for k in "${!known[@]}"; do printf \' %d - %s\\n\' "$((k + 1))" "${known[k]}" >&2; done\n printf \' 0 - назад\\nВаш выбор: \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans >= 1 && 10#$ans <= ${#known[@]})); then\n printf \'sni:%s\' "${known[10#$ans - 1]}"; return 0\n fi\n printf \' ничего не выбрано — оставляю автовыбор\\n\' >&2\n return 0\n fi\n if ((i == own_idx)); then\n printf \' адрес сайта (например vpn.example.com): \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ $ans =~ ^[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$ ]] && { printf \'own:%s\' "$ans"; return 0; }\n printf \' не похоже на адрес сайта — оставляю автовыбор\\n\' >&2\n return 0\n fi\n printf \' такого номера нет\\n\' >&2\n done\n return 0\n}\n\n', 'авто-шов: insert, строк 3', ), ( 'scripts/3x-ui.sh', ' elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 ]] && { : /dev/null; then\n', False, ' # Наш шов: когда выбор сайта делает наше меню (KL_PICK_SNI=1), меню автора не показываем — иначе\n # человек отвечает на «Свой домен / Сосед / Свой сайт», и до скана подсети дело не доходит.\n elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 && ${KL_PICK_SNI:-0} != 1 ]] && { : /dev/null; then\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' echo "Подбираю сайт для маскировки среди соседей по подсети… (около 250 коротких обращений к порту 443 соседних адресов, около минуты; не нужно – запустите с --sni или --domain)"\n', False, ' echo "Сканирую подсеть: около 250 коротких обращений к порту 443 соседних адресов (около минуты)."\n echo "По результатам предложу сайты для маскировки. Не нужно – запустите с --sni или --domain."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' if ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}\n', False, ' # Наш шов: показываем найденное и даём выбрать — отдельно для REALITY и для XHTTP/MTProto.\n if [[ ${KL_PICK_SNI:-} == 1 && $yes == no ]] && { : /dev/null; then\n local pick pick2 own_sni=no\n pick=$(kl_sni_menu "REALITY (TCP 443) — под какой сайт маскировать" ${nb[@]+"${nb[@]}"})\n case $pick in\n own:*)\n own_sni=yes\n DOMAIN=${pick#own:}\n # Наш шов: домен вводится заново, пока он не указывает на этот сервер. Раньше при промахе\n # установка молча маскировалась под чужой сайт, а при домене из флага — обрывалась.\n # A-запись у регистратора расходится не мгновенно, поэтому первым пунктом — «проверить снова».\n while :; do\n if domain_points_here "$DOMAIN"; then\n SNI=$DOMAIN\n ok "маскировка под свой домен: ${B}$DOMAIN${N} (сертификат Let\'s Encrypt выпустит установщик)"\n # Наш шов: спрашиваем, где открывать панель и подписку, и при интерактивном вводе домена тоже.\n # У автора этот вопрос задавался только при домене из флага --domain, а при выборе «ввести свой\n # домен» в меню маскировки — нет, и панель молча оставалась по IP с самоподписанным сертификатом.\n panel_on_choice\n break\n fi\n echo " Домен ${B}$DOMAIN${N} пока не указывает на этот сервер." >&2\n echo " ${B}1${N} проверить снова A-запись расходится по DNS от нескольких минут до нескольких часов" >&2\n echo " ${B}2${N} ввести другой домен" >&2\n echo " ${B}3${N} без своего домена: маскировка соседом по подсети" >&2\n ask_tty " Выбор [1]: " || break\n case ${REPLY//[[:space:]]/} in\n 2) ask_tty " адрес сайта (например vpn.example.com): " || break\n local d=${REPLY//[[:space:]]/}\n [[ -n $d ]] && DOMAIN=$d ;;\n 3) own_sni=no; DOMAIN=""; break ;;\n *) sleep 5 ;;\n esac\n done\n ;;\n sni:*) SNI=${pick#sni:} ;;\n esac\n if [[ -n $SNI ]]; then\n if [[ $own_sni == no ]] && ! sni_ok "$SNI"; then\n warn "$SNI не отвечает по TLS 1.3 + HTTP/2 — беру найденное в подсети"\n SNI="${nb[0]:-$SNI}"\n fi\n ok "маскировка REALITY: ${B}$SNI${N}"\n fi\n if [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n pick2=$(kl_sni_menu "XHTTP и MTProto (различаются по имени сайта) — какой сайт взять" ${nb[@]+"${nb[@]}"})\n [[ $pick2 == sni:* ]] && pick2=${pick2#sni:}\n [[ $pick2 == own:* ]] && pick2=${pick2#own:}\n [[ -n $pick2 ]] && SNI2=$pick2\n [[ -n $SNI2 ]] && ok "маскировка XHTTP/MTProto: ${B}$SNI2${N}"\n fi\n fi\n if ((${#nb[@]})); then ok "подходящих соседей найдено: ${#nb[@]}"\n else warn "в подсети подходящих сайтов не нашлось (адреса проверены, откликов нет)"; fi\n if [[ -z $SNI ]] && ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${SNI2:-${nb[1]:-}}; SNI3=${nb[2]:-}\n', 'авто-шов: replace, строк 2', ), ( 'scripts/3x-ui.sh', ' ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"\n else\n', False, ' ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"\n elif [[ -z $SNI ]]; then\n', 'авто-шов: replace, строк 2', ), ( 'scripts/3x-ui.sh', ' if [[ $yes == no && -t 1 ]] && { : /dev/null; then\n', False, ' if [[ -n ${KL_PICK_SNI:-} && ${KL_PICK_SNI} == 1 && -z $SNI ]]; then\n : # сайт уже выбран в нашем меню — вопрос автора не нужен\n elif [[ $yes == no && -t 1 ]] && { : /dev/null; then\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]]; then\n', False, ' # Наш шов: если XHTTP и MTProto не выбраны, имена для них сейчас не нужны — не сканируем соседей\n # по подсети впустую (около 250 обращений к чужим 443). Имена SNI2/SNI3 возьмутся из общего списка ниже.\n if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]] \\\n && [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя. Исправьте причину и запустите скрипт снова."\n', False, ' # Наш шов: провал сертификата не обрывает установку. Человек может повторить попытку (DNS доехал,\n # лимит Let\'s Encrypt кончился) или ввести другое имя — установка продолжится с него же.\n while ! issue_domain_cert; do\n echo\n echo " ${B}Что делать дальше?${N}"\n echo " ${B}1${N} повторить попытку если ждали обновления A-записи или лимита Let\'s Encrypt"\n echo " ${B}2${N} ввести другой домен проверим, что он указывает на этот сервер, и повторим"\n echo " ${B}3${N} остановиться поставить без своего домена: перезапустите и выберите соседа по подсети"\n ask_tty " Выбор [1]: " || die "Сертификат для $DOMAIN не выпущен. Подробности: /var/log/kit-domain-cert.log"\n case ${REPLY//[[:space:]]/} in\n 2) ask_tty " адрес сайта (например vpn.example.com): " || continue\n local d=${REPLY//[[:space:]]/}\n [[ -n $d ]] || continue\n if domain_points_here "$d"; then DOMAIN=$d; SNI=$DOMAIN\n else warn "оставляю прежний домен $DOMAIN — новый не указывает на этот сервер"; fi ;;\n 3) # Наш шов: продолжаем без своего домена прямо здесь — маскировка соседом по подсети\n # (или известным сайтом), панель и подписка по IP. Перезапускать установку не нужно.\n local nb2\n nb2=$(nearby_sites 2>/dev/null | shuf | head -1)\n [[ -n $nb2 ]] || nb2=dl.google.com\n DOMAIN=""; PANEL_ON=ip; LINK_HOST=$HOST\n SNI=$nb2; SNI2=${SNI2:-$nb2}\n ok "продолжаю без своего домена: маскировка ${B}$SNI${N}, панель и подписка — по IP"\n break ;;\n *) sleep 5 ;;\n esac\n done\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' ok "подключения работают: REALITY, XHTTP и Hysteria2 проверены клиентом с самого сервера"\n', False, ' ok "подключения работают (проверено клиентом с самого сервера): ${CREATED[*]:-нет}"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' local extra\n extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)\n', False, ' local extra="" # пустая строка, иначе set -u валит скрипт, когда AWG/MTProto не выбраны\n # Наш шов: подписки «-awg»/«-tg» есть только у установок с AmneziaWG и MTProto — не тянем их,\n # если эти протоколы не выбраны (никаких лишних обращений к панели).\n if [[ " ${PROTOS[*]} " == *" awg "* || " ${PROTOS[*]} " == *" awg3 "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)\n fi\n', 'авто-шов: replace, строк 2', ), ( 'scripts/3x-ui.sh', ' echo "3X-UI KIT (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"\n', False, ' echo "3X-UI_autoVPS (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' } >"$RESULT"\n\n echo\n echo "${G}${B}Готово! Сервер работает.${N}"\n [[ -n $DOMAIN ]] && echo "Маскировка: свой домен ${B}$DOMAIN${N}, сертификат Let\'s Encrypt продлевается сам."\n [[ $SELF_IP_CERT == yes ]] && later "Сертификат на IP самоподписанный: пользуйтесь ссылками на отдельные подключения, подписка в приложениях может не открыться."\n', False, ' } >"$RESULT"\n\n echo\n', 'авто-шов: delete, строк 6', ), ( 'scripts/3x-ui.sh', ' echo "${B}ПАНЕЛЬ${N}"\n if [[ $TRUSTED == yes ]]; then\n echo " $panel_url"\n else\n echo " Только через SSH-туннель. На вашем компьютере:"\n echo " ssh -L $XUI_PANEL_PORT:127.0.0.1:$XUI_PANEL_PORT root@$HOST"\n echo " Затем в браузере: http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH"\n fi\n echo " Логин: ${B}$XUI_USERNAME${N} Пароль: ${B}$XUI_PASSWORD${N}"\n echo " ${D}(сохранено в $RESULT, файл виден только root)${N}"\n echo\n if [[ $TRUSTED == yes ]]; then\n echo "${B}ПОДПИСКА${N} – одна ссылка для всех протоколов"\n echo " ${B}$SUB_URL${N}"\n echo " Вставьте в Happ, Hiddify, Karing, v2rayN, Clash Verge или FlClash."\n qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true\n else\n /usr/local/bin/kit __links "$NAME" "$SUBID" || true\n fi\n local -a tp=(\n "Управление сервером: введите команду ${B}kit${N} – откроется меню. Запоминать команды не нужно."\n "Ссылки на отдельные протоколы (REALITY, Hysteria2, Telegram и другие): kit → Пользователи → Показать ссылки."\n "Добавить пользователя: kit → Пользователи → Добавить."\n "Резервная копия: kit → Резервная копия. Храните её как пароль.")\n tips "Что дальше" "${tp[@]}"\n', False, ' echo "${G}${B}Установка завершена.${N} Доступы к панели, подписка и QR-код — в самом конце вывода."\n', 'авто-шов: replace, строк 25', ), ( 'scripts/3x-ui.sh', 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\n', False, 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\nKIT_LOCAL_URL="$KIT_RAW/scripts/kit-local.sh"\n', 'авто-шов: insert, строк 1', ), ( 'scripts/3x-ui.sh', ' else curl -fsSL --retry 3 -o /usr/local/bin/kit "$KIT_CLI_URL" && chmod 755 /usr/local/bin/kit; fi\n', False, ' else curl -fsSL --retry 3 -o /usr/local/bin/kit "$KIT_CLI_URL" && chmod 755 /usr/local/bin/kit; fi\n # Форк: ставим наши локальные дополнения (см. scripts/kit-local.sh).\n install -d -m 755 /usr/local/lib/kit\n if [[ -n $src && -f $d/kit-local.sh ]]; then\n install -m 644 "$d/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n else\n curl -fsSL --retry 3 -o /usr/local/lib/kit/kit-local.sh "$KIT_LOCAL_URL" \\\n || warn "Локальные дополнения kit не скачались: kit local не будет работать"\n fi\n', 'авто-шов: insert, строк 1', ), ( 'scripts/3x-ui.sh', 'KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"\n', False, 'KIT_INSTALL_CMD="bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui.sh)"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' echo "# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске."\n', False, ' echo "# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', '# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске.\n', False, '# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске.\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI KIT" --arg uri "$uri" \'\n', False, ' upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI_autoVPS" --arg uri "$uri" \'\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI KIT" \'\n', False, ' upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI_autoVPS" \'\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', 'Description=kit-sub: подписка с учётом приложения (3X-UI KIT)\n', False, 'Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n', False, ' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n', 'авто-шов: replace, строк 2', ), ( 'scripts/3x-ui.sh', ' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n', False, ' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n', 'авто-шов: replace, строк 2', ), ( 'scripts/3x-ui.sh', ' [[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."\n', False, ' [[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' *) die "Это не резервная копия 3X-UI KIT или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;\n', False, ' *) die "Это не резервная копия 3X-UI_autoVPS или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' || die "Это не резервная копия 3X-UI KIT или она повреждена (значение $n в ${f##*/}). Ничего не менял."\n', False, ' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (значение $n в ${f##*/}). Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' python3 - "$file" "$tmp" <<\'PY\' || die "Это не резервная копия 3X-UI KIT или она повреждена. Ничего не менял."\n', False, ' python3 - "$file" "$tmp" <<\'PY\' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена. Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' || die "Это не резервная копия 3X-UI KIT или она повреждена (порт панели). Ничего не менял."\n', False, ' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (порт панели). Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' [[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (адрес сервера). Ничего не менял."\n', False, ' [[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (адрес сервера). Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' || die "Это не резервная копия 3X-UI KIT или она повреждена (нет своего сертификата). Ничего не менял."\n', False, ' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (нет своего сертификата). Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' [[ -n $DOMAIN ]] && { issue_domain_cert || die "Не удалось получить сертификат для $DOMAIN: порт 80 должен быть свободен, а A-запись домена указывать на этот сервер. Лог: /var/log/kit-domain-cert.log"; }\n', False, ' # Наш шов: открываем 80 в ufw ПРЯМО ПЕРЕД выпуском сертификата. Установщик открывает список портов\n # позже (после сборки nginx), поэтому на свежем сервере проверка Let\'s Encrypt доходила до сервера и\n # «проглатывалась» — в логе acme это «Timeout during connect (likely firewall problem)».\n command -v ufw >/dev/null 2>&1 && ufw allow 80/tcp >/dev/null 2>&1 || true\n [[ -n $DOMAIN ]] && { issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя — причина выше, подробности в /var/log/kit-domain-cert.log"; }\n', 'авто-шов: replace, строк 1', ), ( 'scripts/hysteria2.sh', '# установку 3X-UI KIT (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`\n', False, '# установку 3X-UI_autoVPS (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`\n', 'авто-шов: replace, строк 1', ), ( 'scripts/hysteria2.sh', 'Hysteria2 входит в основную установку 3X-UI KIT (один из 11 протоколов).\n', False, 'Hysteria2 входит в основную установку 3X-UI_autoVPS (один из 11 протоколов).\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit-sub.py', ' data = {"Name": "3X-UI KIT", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,\n', False, ' data = {"Name": "3X-UI_autoVPS", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit-sub.py', ' data = {"Name": "3X-UI KIT", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",\n', False, ' data = {"Name": "3X-UI_autoVPS", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit-sub.py', ' main()\n', False, ' main()\n\n', 'авто-шов: insert, строк 1', ), ( 'scripts/kit.sh', version_re, True, f'KIT_VERSION="{version}"', 'версия сборки форка', ), ( 'scripts/3x-ui.sh', version_re, True, f'KIT_VERSION="{version}"', 'версия сборки форка', ), ( 'scripts/hysteria2.sh', version_re, True, f'KIT_VERSION="{version}"', 'версия сборки форка', ), ( 'scripts/kit.sh', 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"', False, 'KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main"', 'канал обновлений: наш Gitea вместо GitHub', ), ] bad = 0 for path, anchor, is_re, new, why in edits: p = pathlib.Path(path) if not p.exists(): print("✗ %s: файла нет" % path) bad += 1 continue text = p.read_text(encoding="utf-8", newline="") if check: # --check смотрит на готовое дерево dev: наш шов обязан быть на месте и ровно один раз n = text.count(new) what, shown = "наш шов", new else: # применение — только на чистое дерево автора: якорь обязан найтись ровно один раз n = count(text, anchor, is_re) what, shown = "якорь автора", anchor short = shown if len(shown) <= 54 else shown[:51] + "…" short = short.replace("\n", "⏎") if n != 1: print("✗ %-20s %-56s %s найден %d раз (%s)" % (path, short, what, n, why)) bad += 1 continue if check: print("✓ %-20s %-56s (%s)" % (path, short, why)) continue if is_re: text = re.sub(anchor, new, text, count=1, flags=re.MULTILINE) else: text = text.replace(anchor, new, 1) p.write_text(text, encoding="utf-8", newline="") print("✓ %-20s %-56s (%s)" % (path, short, why)) # Массовая замена бренда в файлах автора: строки, которые видит человек (справка, версия, # описания служб, баннер). Строку проверки шапки SHA256SUMS не трогаем — её правит отдельный шов, # а сама шапка ещё один релиз пишется в старом виде (иначе уже установленные серверы откажутся # принимать обновление: они сверяют точный вид строки). globals_ = [ ("scripts/kit.sh", "3X-UI KIT", "3X-UI_autoVPS"), ("scripts/3x-ui.sh", "3X-UI KIT", "3X-UI_autoVPS"), ("scripts/hysteria2.sh", "3X-UI KIT", "3X-UI_autoVPS"), ("scripts/kit-sub.py", "3X-UI KIT", "3X-UI_autoVPS"), ] for path, old, new in globals_: p = pathlib.Path(path) if not p.exists(): print("✗ %s: файла нет" % path) bad += 1 continue text = p.read_text(encoding="utf-8", newline="") # строки, где старое имя — часть служебной сверки, не переименовываем keep = lambda line: "grep -qx" in line if check: if new not in text: print("✗ %-20s нет нашего имени «%s»" % (path, new)) bad += 1 continue left = [l for l in text.splitlines() if old in l and not keep(l)] if left: print("✗ %-20s осталось старое имя в %d строк(ах)" % (path, len(left))) bad += 1 continue print("✓ %-20s бренд: %s" % (path, new)) continue out = [] changed = 0 for line in text.splitlines(keepends=True): if old in line and not keep(line): out.append(line.replace(old, new)) changed += line.count(old) else: out.append(line) if not changed: if new in text: print("– %-20s уже переименовано" % path) continue print("✗ %-20s не нашёл «%s»" % (path, old)) bad += 1 continue p.write_text("".join(out), encoding="utf-8", newline="") print("✓ %-20s переименовано вхождений: %d" % (path, changed)) if bad: if check: die("якорей не найдено: %d — автор изменил код, пересмотри правки в tools/fork_apply.py" % bad) die("якорей не найдено: %d — правки не применены" % bad) if check: ok("все якоря на месте (версия сборки %s)" % version) else: ok("швы накатаны, версия сборки %s" % version) if __name__ == "__main__": main()