diff --git a/installer/HermesHubSetup.cs b/installer/HermesHubSetup.cs index 191f042..a117da3 100644 --- a/installer/HermesHubSetup.cs +++ b/installer/HermesHubSetup.cs @@ -18,7 +18,7 @@ namespace HermesHubSetup // Подставляется сборщиком из фактического git-коммита. Раньше здесь // жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из // какого кода собран установщик. - public const string BuildCommit = "884a632"; + public const string BuildCommit = "28f35f8"; public const string MIN_HERMES_VERSION = "0.20.0"; public const string MAX_TESTED_HERMES = "0.20.4"; diff --git a/launcher/HermesHub.exe b/launcher/HermesHub.exe index 9a04c56..f950cd7 100644 Binary files a/launcher/HermesHub.exe and b/launcher/HermesHub.exe differ diff --git a/launcher/HermesHubWeb.exe b/launcher/HermesHubWeb.exe index 8faa9a9..98beb15 100644 Binary files a/launcher/HermesHubWeb.exe and b/launcher/HermesHubWeb.exe differ diff --git a/src/antigravity_provider/agy_subprocess.py b/src/antigravity_provider/agy_subprocess.py index 8d3352f..cf64385 100644 --- a/src/antigravity_provider/agy_subprocess.py +++ b/src/antigravity_provider/agy_subprocess.py @@ -205,15 +205,20 @@ def discover_models(profile_id: str | None = None) -> dict[str, str]: from antigravity_provider.router.adapters.antigravity_adapter import get_profile_env_dir profile_dir = get_profile_env_dir(target_profile_id) - env = build_safe_subprocess_env( - overrides={ - "USERPROFILE": str(profile_dir), - "HOME": str(profile_dir), - "HOMEPATH": str(profile_dir), - } - ) + overrides = { + "USERPROFILE": str(profile_dir), + "HOME": str(profile_dir), + "HOMEPATH": str(profile_dir), + } + # Проверка доступности у Google смотрит на адрес: без выхода через + # разрешённую страну она отвечает «not currently available in your + # location», и каталог моделей получить нельзя. + overrides.update(proxy_env_overrides(resolve_provider_proxy(target_profile_id))) + env = build_safe_subprocess_env(overrides=overrides) else: - env = build_safe_subprocess_env() + env = build_safe_subprocess_env( + overrides=proxy_env_overrides(resolve_provider_proxy()) + ) try: result = subprocess.run( @@ -718,6 +723,53 @@ BLOCKED_SECRET_PATTERNS: tuple[str, ...] = ( ) +def resolve_provider_proxy(profile_id: str | None = None) -> str: + """Адрес прокси для обращений провайдера: сначала профиль, затем общий. + + Google отказывает по местоположению: «not currently available in your + location». Проверка смотрит на адрес, поэтому вопрос решается выходом + через нужную страну, а не правкой чужого бинарника. + + Настройка на профиль важна не для красоты: у владельца несколько + выходных узлов в разных странах, и разным аккаунтам может требоваться + разный. + """ + if profile_id: + try: + from antigravity_provider.router.profile_manager import ProfileAuthManager + + auth = ProfileAuthManager.load_profile_auth("antigravity", profile_id) or {} + own = str(auth.get("proxy_url") or "").strip() + if own: + return own + except Exception as exc: + logger.debug("Прокси профиля %s не прочитан: %s", profile_id, exc) + try: + from antigravity_provider.router.settings_service import get_hub_settings + + return str(get_hub_settings().get("provider_proxy_url") or "").strip() + except Exception as exc: + logger.debug("Общая настройка прокси не прочитана: %s", exc) + return "" + + +def proxy_env_overrides(proxy_url: str) -> dict[str, str]: + """Переменные окружения для прокси. Пустой адрес — пустой набор. + + Пишем и заглавные, и строчные имена: Go читает HTTPS_PROXY, curl и + большинство библиотек — https_proxy. ALL_PROXY нужен для socks5. + """ + url = (proxy_url or "").strip() + if not url: + return {} + names = ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY") + out: dict[str, str] = {} + for name in names: + out[name] = url + out[name.lower()] = url + return out + + def detect_graphical_session() -> tuple[dict[str, str], list[str]]: """Найти графический сеанс владельца. Вернуть (переменные, что проверено). @@ -816,6 +868,19 @@ def write_login_helper(profile_dir: Path, agy_exe: str, profile_id: str) -> Path "USERPROFILE=" + shlex.quote(str(profile_dir)), "HOMEPATH=" + shlex.quote(str(profile_dir)), "export HOME USERPROFILE HOMEPATH", + ] + + # Проверка доступности у Google смотрит на адрес выхода. Без прокси вход + # завершается «Eligibility check failed: not currently available in your + # location» — аккаунт при этом опознан верно, дело только в стране. + proxy = resolve_provider_proxy(profile_id) + if proxy: + for name, value in proxy_env_overrides(proxy).items(): + lines.append(name + "=" + shlex.quote(value)) + lines.append("export " + " ".join(sorted(proxy_env_overrides(proxy)))) + lines.append('echo "Выход через прокси: ' + proxy.replace('"', "") + '"') + + lines += [ "cd " + shlex.quote(str(profile_dir)) + " || exit 1", 'echo "Вход Antigravity в слот ' + profile_id + '"', 'echo "Каталог профиля: $HOME"', diff --git a/src/antigravity_provider/router/adapters/antigravity_adapter.py b/src/antigravity_provider/router/adapters/antigravity_adapter.py index c2b8951..f371005 100644 --- a/src/antigravity_provider/router/adapters/antigravity_adapter.py +++ b/src/antigravity_provider/router/adapters/antigravity_adapter.py @@ -54,14 +54,21 @@ class AntigravityAdapter(BaseProviderAdapter): def invoke(self, profile: RouterProfileConfig, request: Dict[str, Any]) -> Dict[str, Any]: profile_dir = get_profile_env_dir(profile.profile_id) # Isolate USERPROFILE and HOME while strictly stripping non-Antigravity provider secrets - custom_env = build_safe_subprocess_env( - overrides={ - "USERPROFILE": str(profile_dir), - "HOME": str(profile_dir), - "HOMEPATH": str(profile_dir), - } + overrides = { + "USERPROFILE": str(profile_dir), + "HOME": str(profile_dir), + "HOMEPATH": str(profile_dir), + } + # Тот же прокси, что и при входе: иначе аккаунт подключён, а запросы + # упираются в проверку доступности по стране. + from antigravity_provider.agy_subprocess import ( + proxy_env_overrides, + resolve_provider_proxy, ) + overrides.update(proxy_env_overrides(resolve_provider_proxy(profile.profile_id))) + custom_env = build_safe_subprocess_env(overrides=overrides) + # If profile specifies a preferred model and request has generic or no model req = dict(request) model = req.get("model", "") diff --git a/src/antigravity_provider/router/settings_service.py b/src/antigravity_provider/router/settings_service.py index 8fa8970..8a5d650 100644 --- a/src/antigravity_provider/router/settings_service.py +++ b/src/antigravity_provider/router/settings_service.py @@ -6,6 +6,8 @@ along with Obsidian shared memory validation and canonical structure setup. from __future__ import annotations import json +from urllib.parse import urlparse +import re import logging import os import tempfile @@ -36,6 +38,11 @@ DEFAULT_SETTINGS: Dict[str, Any] = { "compression_threshold_percent": 75.0, "compression_keep_recent_messages": 3, "compression_enabled": True, + # Прокси для обращений провайдеров, у которых проверка доступности + # смотрит на адрес. Google отказал владельцу: «not currently available + # in your location». Задаётся адресом вида socks5://127.0.0.1:1080 или + # http://127.0.0.1:8080; пустое значение означает «без прокси». + "provider_proxy_url": "", } @@ -57,6 +64,36 @@ def get_settings_file() -> Path: return get_hermes_home() / "hub_settings.json" +def _normalize_proxy_url(raw: Any) -> str: + """Привести адрес прокси к рабочему виду или отбросить. + + Проверяем не только схему, но и сам адрес: приписать socks5:// можно + чему угодно, и тогда мусор выглядел бы принятым, а все обращения + провайдера молча ломались бы без внятной причины. + """ + value = str(raw or "").strip() + if not value: + return "" + if "://" not in value: + value = "socks5://" + value + try: + parsed = urlparse(value) + except ValueError: + return "" + if parsed.scheme not in ("http", "https", "socks5", "socks5h"): + return "" + host = parsed.hostname or "" + if not host or not re.fullmatch(r"[A-Za-z0-9._\-\[\]:]+", host): + return "" + try: + port = parsed.port + except ValueError: + return "" + if port is not None and not (1 <= port <= 65535): + return "" + return value + + def get_hub_settings() -> Dict[str, Any]: """Load settings from hub_settings.json merged with standard defaults, cached by mtime.""" global _SETTINGS_CACHE, _SETTINGS_CACHE_MTIME, _SETTINGS_CACHE_PATH @@ -139,6 +176,10 @@ def get_hub_settings() -> Dict[str, Any]: except (ValueError, TypeError): merged["compression_threshold_percent"] = 75.0 + # Адрес прокси проверяем на вид, а не принимаем что попало: неверная + # схема тихо ломает все обращения провайдера, и причина не видна. + merged["provider_proxy_url"] = _normalize_proxy_url(merged.get("provider_proxy_url")) + try: merged["compression_keep_recent_messages"] = max(1, min(20, int(merged.get("compression_keep_recent_messages", 3)))) except (ValueError, TypeError): diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 26f7af3..b54e177 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -1727,6 +1727,18 @@ function renderSettingsView() { } } + // Прокси провайдеров: пустая строка — осмысленное значение «без прокси», + // поэтому отличаем её от «сервер не передал». + const proxyInput = document.getElementById('setting-provider-proxy-url'); + if (proxyInput) { + if (s.provider_proxy_url !== undefined) { + proxyInput.value = s.provider_proxy_url || ''; + proxyInput.placeholder = 'без прокси'; + } else { + proxyInput.placeholder = 'Н/Д: не передан сервером'; + } + } + // Quota Interval const quotaIntervalSel = document.getElementById('setting-quota-interval'); if (quotaIntervalSel) { @@ -1829,7 +1841,12 @@ async function saveHubServerSettings() { const defaultRoleSel = document.getElementById('setting-default-role'); const themeSel = document.getElementById('setting-theme'); + const proxyInputSave = document.getElementById('setting-provider-proxy-url'); + const newSettings = {}; + // Пустое поле — это выбор «без прокси», а не отсутствие значения: + // отправляем его тоже, иначе прокси нельзя было бы убрать. + if (proxyInputSave) newSettings.provider_proxy_url = proxyInputSave.value.trim(); if (hostInput && hostInput.value.trim()) newSettings.web_api_host = hostInput.value.trim(); if (portInput && portInput.value) newSettings.web_api_port = Number(portInput.value); if (tokenInput && tokenInput.value.trim()) newSettings.web_api_token = tokenInput.value.trim(); diff --git a/src/antigravity_provider/router/web/static/index.html b/src/antigravity_provider/router/web/static/index.html index 5ffe002..45cefe7 100644 --- a/src/antigravity_provider/router/web/static/index.html +++ b/src/antigravity_provider/router/web/static/index.html @@ -455,6 +455,13 @@ +
socks5://127.0.0.1:1080. Пусто — без прокси. Отдельному аккаунту можно задать свой адрес.