fix: agy не находился в Linux; Ollama Cloud нельзя было подключить по ключу

agy. Поиск проверял только раскладку Windows (%LOCALAPPDATA%/agy/bin) и
PATH. В Linux утилита ставится в ~/.local/bin, а хаб запускается с
урезанным окружением, где этого каталога в PATH нет. Владелец видел «agy
executable not found» при установленной и работающей утилите — which agy
находил её по адресу /home/ochenstarik/.local/bin/agy.

Добавлены стандартные места Linux: ~/.local/bin, /usr/local/bin, /usr/bin,
/snap/bin. Сообщение об отказе перечисляет проверенные пути. Отдельно
различается «нет доступа к каталогу» и «файла нет».

Ollama Cloud. Мастер требовал адрес локального сервера, которого у
облачного аккаунта не существует: там только ключ. Теперь при заданном
ключе недоступность локального адреса не считается отказом — аккаунт
проверяется по каталогу ollama.com и принимается с честной оговоркой,
что локальные модели недоступны. Проверено: 19 моделей каталога.
Без ключа поведение прежнее.

609 passed, ruff clean, релизный гейт 10/10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-09-01 02:07:11 +07:00
parent 266fc51adf
commit f269891271
2 changed files with 62 additions and 6 deletions

View file

@ -46,12 +46,32 @@ def _find_agy_exe() -> str:
if env and Path(env).is_file(): if env and Path(env).is_file():
return env return env
# 2. Standard location based on hermes home parent # 2. Стандартные места установки.
#
# Раньше проверялась только раскладка Windows (%LOCALAPPDATA%/agy/bin), а в
# Linux agy ставится в ~/.local/bin. Хаб запускается с урезанным окружением,
# где этого каталога в PATH нет, и вход в Antigravity падал с «agy executable
# not found» при установленной и работающей утилите.
from antigravity_provider.paths import get_hermes_home from antigravity_provider.paths import get_hermes_home
exe_name = "agy.exe" if os.name == "nt" else "agy" exe_name = "agy.exe" if os.name == "nt" else "agy"
candidate = get_hermes_home().parent / "agy" / "bin" / exe_name candidates = [
get_hermes_home().parent / "agy" / "bin" / exe_name,
Path.home() / ".local" / "bin" / exe_name,
Path("/usr/local/bin") / exe_name,
Path("/usr/bin") / exe_name,
Path("/snap/bin") / exe_name,
]
checked = []
for candidate in candidates:
checked.append(str(candidate))
try:
if candidate.is_file(): if candidate.is_file():
return str(candidate) return str(candidate)
except OSError as exc:
# Каталог может быть закрыт правами: это «не смогли проверить»,
# а не «файла нет».
checked[-1] = f"{candidate} (нет доступа: {exc.strerror or exc})"
# 3. PATH # 3. PATH
found = shutil.which("agy") or shutil.which("agy.exe") found = shutil.which("agy") or shutil.which("agy.exe")
@ -59,8 +79,9 @@ def _find_agy_exe() -> str:
return found return found
raise FileNotFoundError( raise FileNotFoundError(
"agy executable not found. Set the AGY_EXE_PATH environment " "Утилита agy не найдена. Проверено: "
"variable, install agy, or ensure it is on PATH." + "; ".join(checked)
+ "; и PATH процесса. Задайте путь переменной AGY_EXE_PATH либо установите agy."
) )

View file

@ -97,6 +97,41 @@ def validate_connection(provider, token="", base_url="", preferred_model=""):
except Exception as exc: except Exception as exc:
exc_str = str(exc) exc_str = str(exc)
exc_lower = exc_str.lower() exc_lower = exc_str.lower()
# У Ollama Cloud локального сервера нет вовсе: там только ключ. Если
# ключ задан, а локальный адрес не отвечает, это не отказ подключения —
# аккаунт облачный. Проверяем его по общедоступному каталогу и
# принимаем, честно сказав, что локальные модели недоступны.
if provider == "ollama" and token and any(
k in exc_lower for k in ("connection refused", "winerror 10061", "errno 111", "refused", "failed to connect", "target machine actively refused")
):
try:
cloud = urllib.request.Request(
"https://ollama.com/api/tags",
headers={"Accept": "application/json", "Authorization": f"Bearer {token}"},
)
with urllib.request.urlopen(cloud, timeout=15) as response:
payload = json.load(response)
cloud_models = sorted({
m.get("name") for m in (payload.get("models") or [])
if isinstance(m, dict) and isinstance(m.get("name"), str)
})
return {
"ok": True,
"message": (
f"Облачный аккаунт Ollama принят. Моделей в каталоге: {len(cloud_models)}. "
f"Локальный сервер по адресу {base_url} не отвечает — локальные модели недоступны."
),
"data": {"models": cloud_models, "base_url": base_url, "cloud_only": True},
}
except Exception as cloud_exc:
return {
"ok": False,
"message": (
f"Локальный сервер {base_url} не отвечает, и облачный каталог Ollama недоступен: "
f"{str(cloud_exc)[:160]}"
),
"data": {"models": []},
}
if provider == "ollama" and any(k in exc_lower for k in ("connection refused", "winerror 10061", "errno 111", "111", "refused", "failed to connect", "target machine actively refused")): if provider == "ollama" and any(k in exc_lower for k in ("connection refused", "winerror 10061", "errno 111", "111", "refused", "failed to connect", "target machine actively refused")):
return { return {
"ok": False, "ok": False,