feat: проверка обновлений GitHub, пакет utils, зависимости по ОС

Проверка последнего релиза при запуске (опционально), диалог и ссылка на страницу релиза; секция в настройках CTk и меню macOS; единый default_tray_config; requirements/*.txt; README.

Refs: https://github.com/Flowseal/tg-ws-proxy/issues/357
This commit is contained in:
deexsed 2026-03-26 11:21:33 +03:00
parent 35ea33ee3f
commit f0044255fa
14 changed files with 492 additions and 31 deletions

View File

@ -40,10 +40,12 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra
- **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку - **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку
- **Перезапустить прокси** — перезапуск без выхода из приложения - **Перезапустить прокси** — перезапуск без выхода из приложения
- **Настройки...** — GUI-редактор конфигурации - **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
- **Открыть логи** — открыть файл логов - **Открыть логи** — открыть файл логов
- **Выход** — остановить прокси и закрыть приложение - **Выход** — остановить прокси и закрыть приложение
При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках).
### macOS ### macOS
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel. Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
@ -82,6 +84,11 @@ chmod +x TgWsProxy_linux_amd64
## Установка из исходников ## Установка из исходников
Список пакетов `pip` с версиями **по ОС** (Windows/Linux/macOS, отдельно для Python 3.8 на Windows) — в каталоге [`requirements/`](requirements/README.md). Кратко:
- **Linux (трей):** системный пакет Tcl/Tk для `tkinter`, например `python3-tk` (Debian/Ubuntu).
- Дальше: `pip install -r requirements/<файл>.txt` и `pip install -e .` из корня репозитория.
### Консольный proxy ### Консольный proxy
Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки: Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки:
@ -94,6 +101,7 @@ tg-ws-proxy
### Windows 7/10+ ### Windows 7/10+
```bash ```bash
pip install -r requirements/windows-py39plus.txt
pip install -e . pip install -e .
tg-ws-proxy-tray-win tg-ws-proxy-tray-win
``` ```
@ -101,6 +109,7 @@ tg-ws-proxy-tray-win
### macOS ### macOS
```bash ```bash
pip install -r requirements/macos.txt
pip install -e . pip install -e .
tg-ws-proxy-tray-macos tg-ws-proxy-tray-macos
``` ```
@ -108,6 +117,7 @@ tg-ws-proxy-tray-macos
### Linux ### Linux
```bash ```bash
pip install -r requirements/linux.txt
pip install -e . pip install -e .
tg-ws-proxy-tray-linux tg-ws-proxy-tray-linux
``` ```
@ -179,15 +189,22 @@ Tray-приложение хранит данные в:
```json ```json
{ {
"host": "127.0.0.1",
"port": 1080, "port": 1080,
"dc_ip": [ "dc_ip": [
"2:149.154.167.220", "2:149.154.167.220",
"4:149.154.167.220" "4:149.154.167.220"
], ],
"verbose": false "verbose": false,
"buf_kb": 256,
"pool_size": 4,
"log_max_mb": 5.0,
"check_updates": true
} }
``` ```
Ключ **`check_updates`** — при `true` при запросе к GitHub сравнивается версия с последним релизом (только уведомление и ссылка на страницу загрузки). На Windows в конфиге может быть **`autostart`** (автозапуск при входе в систему).
## Автоматическая сборка ## Автоматическая сборка
Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec), [`packaging/linux.spec`](packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec), [`packaging/linux.spec`](packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.

View File

@ -8,6 +8,7 @@ import os
import subprocess import subprocess
import sys import sys
import threading import threading
import webbrowser
import time import time
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
@ -20,6 +21,7 @@ from PIL import Image, ImageDraw, ImageFont
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__ from proxy import __version__
from utils.default_config import default_tray_config
from ui.ctk_tray_ui import ( from ui.ctk_tray_ui import (
install_tray_config_buttons, install_tray_config_buttons,
install_tray_config_form, install_tray_config_form,
@ -44,15 +46,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned" IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
DEFAULT_CONFIG = { DEFAULT_CONFIG = default_tray_config()
"port": 1080,
"host": "127.0.0.1",
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
"verbose": False,
"log_max_mb": 5,
"buf_kb": 256,
"pool_size": 4,
}
_proxy_thread: Optional[threading.Thread] = None _proxy_thread: Optional[threading.Thread] = None
@ -350,6 +344,48 @@ def _show_info(text: str, title: str = "TG WS Proxy"):
root.destroy() root.destroy()
def _ask_yes_no_dialog(text: str, title: str = "TG WS Proxy") -> bool:
import tkinter as _tk
from tkinter import messagebox as _mb
root = _tk.Tk()
root.withdraw()
try:
root.attributes("-topmost", True)
except Exception:
pass
r = _mb.askyesno(title, text, parent=root)
root.destroy()
return bool(r)
def _maybe_notify_update_async():
def _work():
time.sleep(1.5)
if _exiting:
return
if not _config.get("check_updates", True):
return
try:
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
run_check(__version__)
st = get_status()
if not st.get("has_update"):
return
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
ver = st.get("latest") or "?"
text = (
f"Доступна новая версия: {ver}\n\n"
f"Открыть страницу релиза в браузере?"
)
if _ask_yes_no_dialog(text, "TG WS Proxy — обновление"):
webbrowser.open(url)
except Exception as exc:
log.debug("Update check failed: %s", exc)
threading.Thread(target=_work, daemon=True, name="update-check").start()
def _on_open_in_telegram(icon=None, item=None): def _on_open_in_telegram(icon=None, item=None):
host = _config.get("host", DEFAULT_CONFIG["host"]) host = _config.get("host", DEFAULT_CONFIG["host"])
port = _config.get("port", DEFAULT_CONFIG["port"]) port = _config.get("port", DEFAULT_CONFIG["port"])
@ -624,6 +660,8 @@ def run_tray():
start_proxy() start_proxy()
_maybe_notify_update_async()
_show_first_run() _show_first_run()
_check_ipv6_warning() _check_ipv6_warning()

View File

@ -31,6 +31,7 @@ except ImportError:
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__ from proxy import __version__
from utils.default_config import default_tray_config
APP_NAME = "TgWsProxy" APP_NAME = "TgWsProxy"
APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME
@ -40,15 +41,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned" IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png" MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png"
DEFAULT_CONFIG = { DEFAULT_CONFIG = default_tray_config()
"port": 1080,
"host": "127.0.0.1",
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
"verbose": False,
"log_max_mb": 5,
"buf_kb": 256,
"pool_size": 4,
}
_proxy_thread: Optional[threading.Thread] = None _proxy_thread: Optional[threading.Thread] = None
_async_stop: Optional[object] = None _async_stop: Optional[object] = None
@ -427,6 +420,55 @@ def _on_edit_config(_=None):
threading.Thread(target=_edit_config_dialog, daemon=True).start() threading.Thread(target=_edit_config_dialog, daemon=True).start()
def _check_updates_menu_title() -> str:
on = bool(_config.get("check_updates", True))
return (
"✓ Проверять обновления при запуске"
if on
else "Проверять обновления при запуске (выкл)"
)
def _toggle_check_updates(_=None):
global _config
_config["check_updates"] = not bool(_config.get("check_updates", True))
save_config(_config)
if _app is not None:
_app._check_updates_item.title = _check_updates_menu_title()
def _on_open_release_page(_=None):
from utils.update_check import RELEASES_PAGE_URL
webbrowser.open(RELEASES_PAGE_URL)
def _maybe_notify_update_async():
def _work():
time.sleep(1.5)
if _exiting:
return
if not _config.get("check_updates", True):
return
try:
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
run_check(__version__)
st = get_status()
if not st.get("has_update"):
return
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
ver = st.get("latest") or "?"
if _ask_yes_no(
f"Доступна новая версия: {ver}\n\n"
f"Открыть страницу релиза в браузере?",
"TG WS Proxy — обновление",
):
webbrowser.open(url)
except Exception as exc:
log.debug("Update check failed: %s", exc)
threading.Thread(target=_work, daemon=True, name="update-check").start()
# Settings via native macOS dialogs # Settings via native macOS dialogs
def _edit_config_dialog(): def _edit_config_dialog():
cfg = load_config() cfg = load_config()
@ -617,6 +659,12 @@ class TgWsProxyApp(_TgWsProxyAppBase):
self._logs_item = rumps.MenuItem( self._logs_item = rumps.MenuItem(
"Открыть логи", "Открыть логи",
callback=_on_open_logs) callback=_on_open_logs)
self._release_page_item = rumps.MenuItem(
"Страница релиза на GitHub…",
callback=_on_open_release_page)
self._check_updates_item = rumps.MenuItem(
_check_updates_menu_title(),
callback=_toggle_check_updates)
self._version_item = rumps.MenuItem( self._version_item = rumps.MenuItem(
f"Версия {__version__}", f"Версия {__version__}",
callback=lambda _: None) callback=lambda _: None)
@ -633,6 +681,9 @@ class TgWsProxyApp(_TgWsProxyAppBase):
self._settings_item, self._settings_item,
self._logs_item, self._logs_item,
None, None,
self._release_page_item,
self._check_updates_item,
None,
self._version_item, self._version_item,
]) ])
@ -672,6 +723,9 @@ def run_menubar():
return return
start_proxy() start_proxy()
_maybe_notify_update_async()
_show_first_run() _show_first_run()
_check_ipv6_warning() _check_ipv6_warning()

View File

@ -62,7 +62,7 @@ Source = "https://github.com/Flowseal/tg-ws-proxy"
Issues = "https://github.com/Flowseal/tg-ws-proxy/issues" Issues = "https://github.com/Flowseal/tg-ws-proxy/issues"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["proxy", "ui"] packages = ["proxy", "ui", "utils"]
[tool.hatch.build.force-include] [tool.hatch.build.force-include]
"windows.py" = "windows.py" "windows.py" = "windows.py"

66
requirements/README.md Normal file
View File

@ -0,0 +1,66 @@
# Зависимости по ОС (разработка из исходников)
Версии совпадают с [`pyproject.toml`](../pyproject.toml). Для установки **пакета проекта** после зависимостей выполните из корня репозитория:
```bash
pip install -e .
```
Скрипты точек входа: `tg-ws-proxy`, `tg-ws-proxy-tray-win` / `tg-ws-proxy-tray-linux` / `tg-ws-proxy-tray-macos`.
---
## Windows
| Файл | Условие |
|------|---------|
| [`windows-py39plus.txt`](windows-py39plus.txt) | Python **3.9+** (рекомендуется) |
| [`windows-py38.txt`](windows-py38.txt) | Python **3.8** |
**Системно:** для сборки/запуска из исходников — установленный [Python](https://www.python.org/downloads/) с опцией *tcl/tk* (обычно включена). Отдельный Tcl/Tk не требуется.
```powershell
pip install -r requirements\windows-py39plus.txt
pip install -e .
tg-ws-proxy-tray-win
```
---
## Linux
Файл: [`linux.txt`](linux.txt) (Python **3.9+**).
**Системно:** модуль `tkinter` (нужен CustomTkinter для трея):
- Debian/Ubuntu: `sudo apt install python3-tk`
- Fedora: `sudo dnf install python3-tkinter`
- Arch: `sudo pacman -S tk`
Также нужны заголовки Python при сборке расширений (если pip собирает колёса): `python3-dev` / `gcc` — по сообщениям pip.
```bash
pip install -r requirements/linux.txt
pip install -e .
tg-ws-proxy-tray-linux
```
---
## macOS
Файл: [`macos.txt`](macos.txt).
**Системно:** Python с официального установщика или Homebrew; Tcl/Tk обычно уже в комплекте. Для графики трея используется **rumps** (нативная строка меню), не CustomTkinter.
```bash
pip install -r requirements/macos.txt
pip install -e .
tg-ws-proxy-tray-macos
```
---
## Примечание
Готовые бинарники для пользователей — на [странице релизов](https://github.com/Flowseal/tg-ws-proxy/releases); отдельная установка Python и `pip` для них не требуется.

11
requirements/linux.txt Normal file
View File

@ -0,0 +1,11 @@
# Linux (не macOS), Python 3.9+ (как в pyproject.toml)
# Системно: пакет Tcl/Tk для tkinter (CustomTkinter), см. README в этой папке.
# Установка: pip install -r requirements/linux.txt
# Затем из корня репозитория: pip install -e .
pyperclip==1.9.0
psutil==7.0.0
cryptography==46.0.5
Pillow==12.1.1
customtkinter==5.2.2
pystray==0.19.5

9
requirements/macos.txt Normal file
View File

@ -0,0 +1,9 @@
# macOS, Python 3.9+ (без customtkinter/pystray — нативное меню rumps)
# Установка: pip install -r requirements/macos.txt
# Затем из корня репозитория: pip install -e .
pyperclip==1.9.0
psutil==7.0.0
cryptography==46.0.5
Pillow==12.1.0
rumps==0.4.0

View File

@ -0,0 +1,9 @@
# Windows, Python 3.8 (устаревшие пины из pyproject.toml)
# Установка: pip install -r requirements/windows-py38.txt
pyperclip==1.9.0
psutil==5.9.8
cryptography==41.0.7
Pillow==10.4.0
customtkinter==5.2.2
pystray==0.19.5

View File

@ -0,0 +1,10 @@
# Windows, Python 3.9+ (как в pyproject.toml для platform_system == Windows, python_version >= 3.9)
# Установка: pip install -r requirements/windows-py39plus.txt
# Затем из корня репозитория: pip install -e .
pyperclip==1.9.0
psutil==7.0.0
cryptography==46.0.5
Pillow==12.1.1
customtkinter==5.2.2
pystray==0.19.5

View File

@ -5,11 +5,13 @@
from __future__ import annotations from __future__ import annotations
import webbrowser
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__ from proxy import __version__
from utils.update_check import RELEASES_PAGE_URL, get_status
from ui.ctk_theme import ( from ui.ctk_theme import (
FIRST_RUN_FRAME_PAD, FIRST_RUN_FRAME_PAD,
@ -52,6 +54,10 @@ _TIP_AUTOSTART = (
"Запускать TG WS Proxy при входе в Windows. " "Запускать TG WS Proxy при входе в Windows. "
"Если вы переместите программу в другую папку, запись автозапуска может сброситься." "Если вы переместите программу в другую папку, запись автозапуска может сброситься."
) )
_TIP_CHECK_UPDATES = (
"При запуске запрашивать с GitHub информацию о последнем релизе. "
"При появлении новой версии можно открыть страницу загрузки."
)
_TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси." _TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси."
_TIP_CANCEL = "Закрыть окно без сохранения изменений." _TIP_CANCEL = "Закрыть окно без сохранения изменений."
@ -121,6 +127,7 @@ class TrayConfigFormWidgets:
adv_entries: List[Any] adv_entries: List[Any]
adv_keys: Tuple[str, ...] adv_keys: Tuple[str, ...]
autostart_var: Optional[Any] autostart_var: Optional[Any]
check_updates_var: Optional[Any]
def install_tray_config_form( def install_tray_config_form(
@ -291,6 +298,71 @@ def install_tray_config_form(
adv_entries = list(adv_frame.winfo_children()) adv_entries = list(adv_frame.winfo_children())
adv_keys = ("buf_kb", "pool_size", "log_max_mb") adv_keys = ("buf_kb", "pool_size", "log_max_mb")
upd_inner = _config_section(ctk, frame, theme, "Обновления")
st = get_status()
check_updates_var = ctk.BooleanVar(
value=bool(
cfg.get("check_updates", default_config.get("check_updates", True))
)
)
upd_cb = ctk.CTkCheckBox(
upd_inner,
text="Проверять обновления при запуске",
variable=check_updates_var,
font=(theme.ui_font_family, 13),
text_color=theme.text_primary,
fg_color=theme.tg_blue,
hover_color=theme.tg_blue_hover,
corner_radius=6,
border_width=2,
border_color=theme.field_border,
)
upd_cb.pack(anchor="w", pady=(0, 6))
attach_ctk_tooltip(upd_cb, _TIP_CHECK_UPDATES)
if st.get("error"):
upd_status = "Не удалось связаться с GitHub. Проверьте сеть."
elif not st.get("checked"):
upd_status = "Статус появится после фоновой проверки при запуске."
elif st.get("has_update") and st.get("latest"):
upd_status = (
f"На GitHub доступна версия {st['latest']} "
f"(у вас {__version__})."
)
elif st.get("ahead_of_release") and st.get("latest"):
upd_status = (
f"У вас {__version__} — новее последнего релиза на GitHub "
f"({st['latest']})."
)
else:
upd_status = "Установлена последняя известная версия с GitHub."
ctk.CTkLabel(
upd_inner,
text=upd_status,
font=(theme.ui_font_family, 11),
text_color=theme.text_secondary,
anchor="w",
justify="left",
wraplength=inner_w,
).pack(anchor="w", pady=(0, 8))
rel_url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
open_rel_btn = ctk.CTkButton(
upd_inner,
text="Открыть страницу релиза",
height=32,
font=(theme.ui_font_family, 13),
corner_radius=8,
fg_color=theme.field_bg,
hover_color=theme.field_border,
text_color=theme.text_primary,
border_width=1,
border_color=theme.field_border,
command=lambda u=rel_url: webbrowser.open(u),
)
open_rel_btn.pack(anchor="w")
autostart_var = None autostart_var = None
if show_autostart: if show_autostart:
sys_inner = _config_section( sys_inner = _config_section(
@ -330,6 +402,7 @@ def install_tray_config_form(
adv_entries=adv_entries, adv_entries=adv_entries,
adv_keys=adv_keys, adv_keys=adv_keys,
autostart_var=autostart_var, autostart_var=autostart_var,
check_updates_var=check_updates_var,
) )
@ -399,6 +472,8 @@ def validate_config_form(
) )
merge_adv_from_form(widgets, new_cfg, default_config) merge_adv_from_form(widgets, new_cfg, default_config)
if widgets.check_updates_var is not None:
new_cfg["check_updates"] = bool(widgets.check_updates_var.get())
return new_cfg return new_cfg

5
utils/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""Вспомогательные утилиты (проверка релизов и т.п.)."""
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
__all__ = ["RELEASES_PAGE_URL", "get_status", "run_check"]

27
utils/default_config.py Normal file
View File

@ -0,0 +1,27 @@
"""
Общие значения по умолчанию для tray-приложений (Windows / Linux / macOS).
Единственное отличие по платформе ключ autostart только на Windows.
"""
from __future__ import annotations
import sys
from typing import Any, Dict
_TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
"port": 1080,
"host": "127.0.0.1",
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
"verbose": False,
"check_updates": True,
"log_max_mb": 5,
"buf_kb": 256,
"pool_size": 4,
}
def default_tray_config() -> Dict[str, Any]:
"""Новая копия конфига по умолчанию для текущей ОС."""
cfg = dict(_TRAY_DEFAULTS_COMMON)
if sys.platform == "win32":
cfg["autostart"] = False
return cfg

102
utils/update_check.py Normal file
View File

@ -0,0 +1,102 @@
"""
Минимальная проверка новой версии через GitHub Releases API (без сторонних зависимостей).
"""
from __future__ import annotations
import json
from itertools import zip_longest
from typing import Any, Dict, Optional
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
REPO = "Flowseal/tg-ws-proxy"
RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
RELEASES_PAGE_URL = f"https://github.com/{REPO}/releases/latest"
_state: Dict[str, Any] = {
"checked": False,
"has_update": False,
"ahead_of_release": False,
"latest": None,
"html_url": None,
"error": None,
}
def _parse_version_tuple(s: str) -> tuple:
s = (s or "").strip().lstrip("vV")
if not s:
return (0,)
parts = []
for seg in s.split("."):
digits = "".join(c for c in seg if c.isdigit())
if digits:
try:
parts.append(int(digits))
except ValueError:
parts.append(0)
else:
parts.append(0)
return tuple(parts) if parts else (0,)
def _version_gt(a: str, b: str) -> bool:
"""True, если версия a новее b (простое сравнение по сегментам)."""
ta = _parse_version_tuple(a)
tb = _parse_version_tuple(b)
for x, y in zip_longest(ta, tb, fillvalue=0):
if x > y:
return True
if x < y:
return False
return False
def fetch_latest_release(timeout: float = 12.0) -> Optional[dict]:
req = Request(
RELEASES_LATEST_API,
headers={
"Accept": "application/vnd.github+json",
"User-Agent": "tg-ws-proxy-update-check",
},
method="GET",
)
with urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
return json.loads(raw)
def run_check(current_version: str) -> None:
"""Запрашивает последний релиз и обновляет внутреннее состояние."""
global _state
_state["checked"] = True
_state["error"] = None
try:
data = fetch_latest_release()
tag = (data.get("tag_name") or "").strip()
html_url = (data.get("html_url") or "").strip() or RELEASES_PAGE_URL
if not tag:
_state["has_update"] = False
_state["ahead_of_release"] = False
_state["latest"] = None
_state["html_url"] = html_url
return
latest_clean = tag.lstrip("vV")
cur = (current_version or "").strip().lstrip("vV")
_state["latest"] = latest_clean
_state["html_url"] = html_url
_state["has_update"] = _version_gt(latest_clean, cur)
_state["ahead_of_release"] = bool(latest_clean) and _version_gt(
cur, latest_clean
)
except (HTTPError, URLError, OSError, TimeoutError, ValueError, json.JSONDecodeError) as e:
_state["error"] = str(e)
_state["has_update"] = False
_state["ahead_of_release"] = False
_state["latest"] = None
_state["html_url"] = RELEASES_PAGE_URL
def get_status() -> Dict[str, Any]:
"""Снимок состояния после run_check (для подписей в настройках)."""
return dict(_state)

View File

@ -38,6 +38,7 @@ except ImportError:
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__ from proxy import __version__
from utils.default_config import default_tray_config
from ui.ctk_tray_ui import ( from ui.ctk_tray_ui import (
install_tray_config_buttons, install_tray_config_buttons,
install_tray_config_form, install_tray_config_form,
@ -65,16 +66,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned" IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
DEFAULT_CONFIG = { DEFAULT_CONFIG = default_tray_config()
"port": 1080,
"host": "127.0.0.1",
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
"verbose": False,
"autostart": False,
"log_max_mb": 5,
"buf_kb": 256,
"pool_size": 4,
}
_proxy_thread: Optional[threading.Thread] = None _proxy_thread: Optional[threading.Thread] = None
@ -404,6 +396,50 @@ def _show_info(text: str, title: str = "TG WS Proxy"):
_user32.MessageBoxW(None, text, title, 0x40) _user32.MessageBoxW(None, text, title, 0x40)
def _ask_open_release_page(latest_version: str, url: str) -> bool:
"""Win32 Yes/No: открыть страницу релиза."""
MB_YESNO = 0x4
MB_ICONQUESTION = 0x20
IDYES = 6
text = (
f"Доступна новая версия: {latest_version}\n\n"
f"Открыть страницу релиза в браузере?"
)
r = _user32.MessageBoxW(
None,
text,
"TG WS Proxy — обновление",
MB_YESNO | MB_ICONQUESTION,
)
return r == IDYES
def _maybe_notify_update_async():
"""
Фоновая проверка GitHub Releases и уведомление (не блокирует трей).
"""
def _work():
time.sleep(1.5)
if _exiting:
return
if not _config.get("check_updates", True):
return
try:
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
run_check(__version__)
st = get_status()
if not st.get("has_update"):
return
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
ver = st.get("latest") or "?"
if _ask_open_release_page(str(ver), url):
webbrowser.open(url)
except Exception as exc:
log.debug("Update check failed: %s", exc)
threading.Thread(target=_work, daemon=True, name="update-check").start()
def _on_open_in_telegram(icon=None, item=None): def _on_open_in_telegram(icon=None, item=None):
host = _config.get("host", DEFAULT_CONFIG["host"]) host = _config.get("host", DEFAULT_CONFIG["host"])
port = _config.get("port", DEFAULT_CONFIG["port"]) port = _config.get("port", DEFAULT_CONFIG["port"])
@ -705,6 +741,8 @@ def run_tray():
start_proxy() start_proxy()
_maybe_notify_update_async()
_show_first_run() _show_first_run()
_check_ipv6_warning() _check_ipv6_warning()