fix(accounts): чужая почта после смены аккаунта, отказ NVIDIA при верном ключе, отмена выхода
Кэш опознания. _identities и _snapshots в AccountQuotaService живут в памяти по ключу «провайдер:слот» и при удалении ключа не чистились. Владелец удалил аккаунт Antigravity, завёл victor.trushenko@gmail.com, а в списке остался прежний trushenko.semya@gmail.com. Добавлен forget_profile, вызывается при удалении учётных данных и при перезаписи слота другим аккаунтом. Проверено: после сброса запись исчезает. NVIDIA. Проверка подключения получала каталог моделей успешно — то есть ключ рабочий и аккаунт опознан, — а затем делала пробный запрос к первой чат-модели каталога. Каталог NVIDIA общий, доступ к конкретной модели даётся по аккаунту, и ответ «Function ... Not found for account <id>» объявлялся провалом подключения. Теперь успешный список моделей считается доказательством работоспособности ключа, а неудачная проба возвращается примечанием с предложением выбрать доступную модель. 401 и 403 при этом по-прежнему означают отказ: ключ отвергнут. Различие поймал тест A54 на неверном ключе. Выход из программы. Неудачная остановка процессов отменяла выход целиком: владелец нажимал «закрыть» и оставался в работающем приложении. Теперь показывается предупреждение, а программа закрывается. 599 passed, ruff clean, релизный гейт 10/10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b2ca7cdd4d
commit
e74d4fbe4a
4 changed files with 55 additions and 9 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
|
|
@ -336,7 +336,13 @@ namespace HermesHub
|
|||
kill.UseShellExecute = false; kill.CreateNoWindow = true;
|
||||
using (Process process = Process.Start(kill)) { process.WaitForExit(5000); }
|
||||
}
|
||||
StopOwnedRuntime(home, false);
|
||||
// Неудачная остановка не повод отменять выход: владелец нажал
|
||||
// «закрыть», и программа обязана закрыться. Прежде окно с
|
||||
// ошибкой возвращало его обратно в работающее приложение.
|
||||
try { StopOwnedRuntime(home, false); }
|
||||
catch (Exception stopEx) {
|
||||
MessageBox.Show("Часть процессов остановить не удалось: " + stopEx.Message + " Hermes Hub закроется; при необходимости снимите их в диспетчере задач.", "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
tray.Visible = false; tray.Dispose(); timer.Dispose(); ExitThread();
|
||||
} catch (Exception ex) {
|
||||
closing = false; timer.Start();
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ def do_delete_credentials(provider: str, profile_id: str, actor: str = "system")
|
|||
try:
|
||||
auth_p.unlink()
|
||||
from .state_store import HubStateStore
|
||||
from antigravity_provider.router.quota_collector import AccountQuotaService
|
||||
AccountQuotaService.get().forget_profile(provider, profile_id)
|
||||
HubStateStore.get().apply_delta_account_removed(provider, profile_id)
|
||||
EventLogService.get().log(
|
||||
'account',
|
||||
|
|
@ -835,6 +837,10 @@ class ActionExecutor:
|
|||
# Повторный вызов здесь обращался к auth_data, которой у уже
|
||||
# авторизованного аккаунта не существует: перевод аккаунта в
|
||||
# другую роль падал с ошибкой, хотя ключ вводить не требуется.
|
||||
# Слот мог принадлежать другому аккаунту: старое опознание
|
||||
# обязано уйти вместе с прежними учётными данными.
|
||||
from antigravity_provider.router.quota_collector import AccountQuotaService
|
||||
AccountQuotaService.get().forget_profile(prov_norm, slot)
|
||||
AutoAssigner.assign_profile_to_role(slot, target_role, is_primary=False)
|
||||
if validation:
|
||||
from .model_discovery_service import ModelDiscoveryService
|
||||
|
|
|
|||
|
|
@ -63,14 +63,35 @@ def validate_connection(provider, token="", base_url="", preferred_model=""):
|
|||
raise ValueError("Каталог NVIDIA пуст: проверить ключ тестовым запросом невозможно")
|
||||
if preferred_model and preferred_model not in models:
|
||||
raise ValueError("Выбранной модели нет в каталоге NVIDIA")
|
||||
# NVIDIA also exposes a public catalog. Validate with a real request.
|
||||
# Каталог NVIDIA общий для всех, а доступ к конкретной модели даётся
|
||||
# по аккаунту. Успешный список уже доказывает, что ключ рабочий:
|
||||
# сервер опознал аккаунт и ответил. Пробный запрос — уточнение, а не
|
||||
# условие. Раньше его отказ («Function ... Not found for account»)
|
||||
# объявлялся провалом подключения, хотя ключ был верным.
|
||||
chat_models = [model for model in models if any(word in model.lower() for word in ('instruct', 'chat')) and not any(word in model.lower() for word in ('embed', 'guard', 'reward'))]
|
||||
if not preferred_model and not chat_models:
|
||||
raise ValueError("Ключ пока не проверен: в каталоге NVIDIA не найдена чат-модель для теста")
|
||||
result = request("/chat/completions", {"model": preferred_model or chat_models[0],
|
||||
"messages": [{"role": "user", "content": "ping"}], "max_tokens": 1})
|
||||
if not isinstance(result.get("choices"), list) or not result["choices"]:
|
||||
raise ValueError("NVIDIA не вернула результат тестового запроса")
|
||||
probe_target = preferred_model or (chat_models[0] if chat_models else None)
|
||||
if probe_target:
|
||||
try:
|
||||
result = request("/chat/completions", {"model": probe_target,
|
||||
"messages": [{"role": "user", "content": "ping"}], "max_tokens": 1})
|
||||
if not isinstance(result.get("choices"), list) or not result["choices"]:
|
||||
probe_note = f"модель {probe_target} не вернула результат"
|
||||
else:
|
||||
probe_note = ""
|
||||
except Exception as probe_exc:
|
||||
# 401 и 403 — ключ отвергнут, это отказ подключения.
|
||||
# 404 и прочее — ключ принят, но модель аккаунту не выдана:
|
||||
# NVIDIA отвечает «Function ... Not found for account <id>»,
|
||||
# то есть аккаунт опознан. Валить подключение из-за этого нельзя.
|
||||
code = getattr(probe_exc, "code", None)
|
||||
if code in (401, 403):
|
||||
raise
|
||||
probe_note = f"модель {probe_target} недоступна вашему аккаунту ({str(probe_exc)[:120]})"
|
||||
else:
|
||||
probe_note = "чат-модель для пробы не найдена"
|
||||
if probe_note:
|
||||
return {"ok": True, "data": {"models": models, "base_url": base_url},
|
||||
"message": f"Ключ принят, моделей в каталоге: {len(models)}. Проба: {probe_note}. Выберите доступную модель."}
|
||||
message = f"Подключено и проверено. Получено моделей: {len(models)}" if models else "Сервер отвечает; моделей пока нет"
|
||||
return {"ok": True, "message": message, "data": {"models": models, "base_url": base_url}}
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -69,6 +69,19 @@ class AccountQuotaService:
|
|||
# If not in cache, generate baseline snapshot from profile auth
|
||||
return self._generate_baseline_snapshot(provider, profile_id)
|
||||
|
||||
def forget_profile(self, provider: str, profile_id: str) -> None:
|
||||
"""Забыть опознание и квоты профиля.
|
||||
|
||||
_identities и _snapshots живут в памяти по ключу «провайдер:слот».
|
||||
При удалении ключа они не чистились, и слот, переиспользованный под
|
||||
другой аккаунт, показывал прежнюю почту: владелец удалил один
|
||||
аккаунт Antigravity, завёл другой и увидел в списке старый адрес.
|
||||
"""
|
||||
key = f"{provider}:{profile_id}"
|
||||
with self._cache_lock:
|
||||
self._identities.pop(key, None)
|
||||
self._snapshots.pop(key, None)
|
||||
|
||||
def get_identity(self, provider: str, profile_id: str) -> AccountIdentity:
|
||||
key = f"{provider}:{profile_id}"
|
||||
with self._cache_lock:
|
||||
|
|
|
|||
Loading…
Reference in a new issue