fix(web): подключённый аккаунт не появлялся в списке

Владелец подключил первый аккаунт Antigravity на сервере, получил
«Авторизация успешно завершена» — и не увидел его в разделе «Аккаунты».

Учётные данные сохранялись правильно: save_profile_auth и load_profile_auth
симметричны, get_profile_status после записи отдаёт authenticated=True —
проверено исполнением. Не совпадало другое: состояние профилей берётся из
кэша UnifiedHealthService, а фоновый цикл веб-сервера обновляет снапшот с
force_scan=False и кэш не трогает.

Измерено на изолированном каталоге:

    до входа                     ag-w1 -> not_configured
    сразу после входа            ag-w1 -> not_configured
    после refresh(force=False)   ag-w1 -> not_configured   <- цикл делает это
    после refresh(force=True)    ag-w1 -> not_tested

То есть аккаунт не появился бы никогда, пока хаб не перезапустят.

Теперь после успешного входа выполняется пересбор с force_scan=True — во всех
четырёх точках завершения: device-flow, ручная вставка адреса, опрос
redirect-потока и код Claude. Ошибка пересбора логируется и сам вход не
роняет.

Функция объявлена на уровне модуля: вложенной она была видна не всем точкам,
и device-flow получал NameError внутри обработки успеха. Мой тест этого не
поймал, потому что проверял только redirect-путь — нашла проверка ruff.

Проверено: not_configured -> not_tested сразу после входа, без ручного
обновления; путь device-flow исполняется без NameError. 432 passed, ruff
чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-25 11:16:27 +07:00
parent 0800ca94e0
commit 681c899cfd

View file

@ -236,6 +236,27 @@ def do_set_model(profile_id: str, model: str, role_id: Optional[str] = None) ->
return False, "Не удалось сохранить файл конфигурации" return False, "Не удалось сохранить файл конфигурации"
# Подключённый аккаунт обязан появиться в списке сразу.
#
# Учётные данные сохраняются на диск, но состояние профилей берётся из кэша
# UnifiedHealthService, а фоновый цикл веб-сервера обновляет снапшот с
# force_scan=False, то есть кэш не трогает. Проверено исполнением: после входа
# профиль оставался not_configured и при обычном refresh, и переходил в
# not_tested только при force_scan=True. Владелец видел «аккаунт подключён» и
# пустой список аккаунтов.
#
# Функция объявлена на уровне модуля намеренно: как вложенная она была видна
# не всем точкам завершения входа, и device-flow получал NameError внутри
# обработки успеха.
def _rescan_after_auth() -> None:
try:
from antigravity_provider.router.state_store import HubStateStore
HubStateStore.get().refresh(force_scan=True)
except Exception as exc: # пересбор не должен ронять сам вход
logger.warning("Не удалось пересобрать снапшот после входа: %s", exc)
class ActionExecutor: class ActionExecutor:
"""Shared execution layer for Desktop and Web actions.""" """Shared execution layer for Desktop and Web actions."""
@ -310,6 +331,7 @@ class ActionExecutor:
status = getattr(session, 'status', 'unknown') status = getattr(session, 'status', 'unknown')
if status == 'completed': if status == 'completed':
_rescan_after_auth()
return {'ok': True, 'message': 'Аккаунт подключён', 'data': {'status': status}} return {'ok': True, 'message': 'Аккаунт подключён', 'data': {'status': status}}
if status in ('failed', 'timeout'): if status in ('failed', 'timeout'):
reason = getattr(session, 'error_msg', None) or 'Авторизация не завершена' reason = getattr(session, 'error_msg', None) or 'Авторизация не завершена'
@ -402,6 +424,8 @@ class ActionExecutor:
if not session: if not session:
return {'ok': False, 'message': 'Сессия авторизации не найдена или уже завершена'} return {'ok': False, 'message': 'Сессия авторизации не найдена или уже завершена'}
ok, msg = session.handle_auth_code(raw_value) ok, msg = session.handle_auth_code(raw_value)
if ok:
_rescan_after_auth()
else: else:
from antigravity_provider.router.profile_oauth import get_oauth_session from antigravity_provider.router.profile_oauth import get_oauth_session
@ -409,6 +433,8 @@ class ActionExecutor:
if not session: if not session:
return {'ok': False, 'message': 'Сессия авторизации не найдена или уже завершена'} return {'ok': False, 'message': 'Сессия авторизации не найдена или уже завершена'}
ok, msg = session.handle_manual_callback_url(raw_value) ok, msg = session.handle_manual_callback_url(raw_value)
if ok:
_rescan_after_auth()
return {'ok': ok, 'message': msg} return {'ok': ok, 'message': msg}
if action == 'poll_redirect_auth': if action == 'poll_redirect_auth':
@ -420,6 +446,7 @@ class ActionExecutor:
return {'ok': False, 'message': 'Сессия авторизации не найдена'} return {'ok': False, 'message': 'Сессия авторизации не найдена'}
status = getattr(session, 'status', 'unknown') status = getattr(session, 'status', 'unknown')
if status == 'completed': if status == 'completed':
_rescan_after_auth()
return {'ok': True, 'message': 'Аккаунт подключён', 'data': {'status': 'completed'}} return {'ok': True, 'message': 'Аккаунт подключён', 'data': {'status': 'completed'}}
if status in ('failed', 'cancelled'): if status in ('failed', 'cancelled'):
return {'ok': False, 'message': session.error_msg or 'Авторизация не удалась'} return {'ok': False, 'message': session.error_msg or 'Авторизация не удалась'}