From 0be0a58a5d61fa5b367a1578ad446dcf382163c8 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Mon, 24 Aug 2026 15:20:05 +0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=20=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20=D0=B4=D0=BB=D1=8F=20Grok=20=D0=B8=20Codex?= =?UTF-8?q?=20=D0=BF=D1=80=D1=8F=D0=BC=D0=BE=20=D0=B8=D0=B7=20=D0=B2=D0=B5?= =?UTF-8?q?=D0=B1=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Владелец упёрся в заглушку «подключение через веб-интерфейс пока не реализовано» и не смог подключить Grok. Backend был готов давно — start_grok_oauth и start_codex_oauth возвращают настоящие адрес и код, — но наружу не выведен: подключить эти провайдеры можно было только из десктопа. Добавлены действия start_device_auth и poll_device_auth. Адрес и код выдаёт ПРОВАЙДЕР, интерфейс их только отображает — никаких подставленных значений, как было с выдуманными GRK-7842 и CDX-9104. Клиент показывает ссылку с кнопками «Открыть» и «Копировать», код крупно и моноширинным, и опрашивает состояние каждые три секунды. Отказ и просроченный код показываются как окончательные, опрос прекращается — это работает вместе с правкой 50e4de5, научившей опрос различать authorization_pending, access_denied и expired_token. Проверено через веб-API: start отдаёт настоящий адрес accounts.x.ai и код, poll возвращает pending, несуществующая сессия — честный отказ. Контракт поднят до 1.4, действий стало двадцать одно. Это снимает главное препятствие к удалению десктопа: он был единственным путём подключить Grok и Codex. Тесты: 414 passed, ruff чисто. Co-Authored-By: Claude Opus 5 --- docs/web-api/CONTRACT.md | 7 ++ .../router/action_handler.py | 57 ++++++++++ .../router/web/static/app.js | 100 +++++++++++++++--- 3 files changed, 148 insertions(+), 16 deletions(-) diff --git a/docs/web-api/CONTRACT.md b/docs/web-api/CONTRACT.md index b3f6482..f6483c6 100644 --- a/docs/web-api/CONTRACT.md +++ b/docs/web-api/CONTRACT.md @@ -113,6 +113,13 @@ test - **`open_routing`** и **`account_details`** в вебе — навигация, состояние держит клиент; сервер на них отвечает `ok: true` без побочных эффектов. - **`refresh_models`** (добавлено в A23): принудительное обновление списка моделей провайдера. `data: {"provider": ""}`. Обнаружение ходит в сеть и подпроцесс — действие возвращается сразу, результат приходит следующим снапшотом. + +**`start_device_auth` / `poll_device_auth`** (добавлено при разборе жалобы «не даёт зайти в грок»): авторизация по коду устройства для Grok и OpenAI Codex прямо из веба. Раньше поток был только в десктопе, а веб-мастер показывал заглушку. + +`start_device_auth` — `data: {"provider": "grok"|"openai-codex"}`. Возвращает `{"session_id", "url", "code", "profile_id"}`. **Адрес и код выдаёт провайдер**; интерфейс их только отображает и не имеет права подставлять свои. + +`poll_device_auth` — `data: {"provider", "session_id"}`. `ok: true` со `status: "pending"` означает ожидание подтверждения; `status: "completed"` — аккаунт подключён; `ok: false` — конечный отказ с причиной (пользователь отклонил, код истёк, сессия не найдена). Отказ и просрочку клиент обязан показывать как окончательные и прекращать опрос. + ### `GET /api/health` `{"ok": true, "version": "<версия Hub>", "auth_flows": {...}}`. Без авторизации — нужен для проверки, что сервер поднялся. diff --git a/src/antigravity_provider/router/action_handler.py b/src/antigravity_provider/router/action_handler.py index 2c44b36..5878af2 100644 --- a/src/antigravity_provider/router/action_handler.py +++ b/src/antigravity_provider/router/action_handler.py @@ -238,6 +238,63 @@ class ActionExecutor: """ pid = data.get('profile_id', '') prov = data.get('provider', '') + + # Device-flow для Grok и Codex через веб. Backend был готов давно, но + # наружу не выведен: веб-мастер показывал заглушку «не реализовано», и + # подключить эти провайдеры можно было только из десктопа. Настоящие + # адрес и код выдаёт провайдер — интерфейс их только отображает. + if action == 'start_device_auth': + provider = (data.get('provider') or '').strip().lower() + slot = data.get('profile_id') or AutoAssigner.find_free_slot(provider) + if not slot: + return {'ok': False, 'message': f'Нет свободного слота для провайдера {provider}'} + try: + if provider == 'grok': + from antigravity_provider.router.grok_oauth import start_grok_oauth + + session_id, url, code = start_grok_oauth(slot) + elif provider in ('openai-codex', 'codex'): + from antigravity_provider.router.codex_oauth import start_codex_oauth + + session_id, url, code = start_codex_oauth(slot) + else: + return {'ok': False, 'message': f'Провайдер {provider} не использует код устройства'} + except Exception as exc: + return {'ok': False, 'message': f'Не удалось начать авторизацию: {exc}'} + + if not code: + return {'ok': False, 'message': 'Провайдер не выдал код устройства'} + return { + 'ok': True, + 'message': 'Код устройства получен', + 'data': {'session_id': session_id, 'url': url, 'code': code, 'profile_id': slot}, + } + + if action == 'poll_device_auth': + provider = (data.get('provider') or '').strip().lower() + session_id = data.get('session_id') or '' + if provider == 'grok': + from antigravity_provider.router.grok_oauth import get_grok_oauth_session + + session = get_grok_oauth_session(session_id) + elif provider in ('openai-codex', 'codex'): + from antigravity_provider.router.codex_oauth import get_codex_oauth_session + + session = get_codex_oauth_session(session_id) + else: + return {'ok': False, 'message': f'Провайдер {provider} не использует код устройства'} + + if session is None: + return {'ok': False, 'message': 'Сессия авторизации не найдена или истекла'} + + status = getattr(session, 'status', 'unknown') + if status == 'completed': + return {'ok': True, 'message': 'Аккаунт подключён', 'data': {'status': status}} + if status in ('failed', 'timeout'): + reason = getattr(session, 'error_msg', None) or 'Авторизация не завершена' + return {'ok': False, 'message': reason, 'data': {'status': status}} + return {'ok': True, 'message': 'Ожидание подтверждения', 'data': {'status': status}} + # Подключение аккаунта: для локального сервера это не навигация, а # настоящее сохранение профиля с адресом. if action == 'add_account': diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 88dd88d..e8b98e0 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -1566,28 +1566,19 @@ function showWizardStep2(providerId) { let bodyHtml = ''; if (providerId === 'grok' || providerId === 'openai-codex') { - // Здесь стояли ВЫДУМАННЫЕ код устройства (GRK-7842 / CDX-9104) и жёстко - // вписанный адрес x.ai/device, который отдаёт 404. Мастер не был подключён - // к серверу вовсе: пользователь вводил бы несуществующий код бесконечно. - // Пока поток не проведён через API, честнее сказать правду и указать - // рабочий путь, чем показывать правдоподобную пустышку. + // Поток кода устройства проведён через веб-API. Адрес и код приходят от + // ПРОВАЙДЕРА и подставляются сюда; ничего не вписано в код. Раньше здесь + // стояли выдуманные GRK-7842 и CDX-9104 при жёстко вписанном адресе. const providerName = providerId === 'grok' ? 'Grok (xAI)' : 'OpenAI Codex'; bodyHtml = `
- Шаг 2 из 3: Авторизация ${providerName} + Шаг 2 из 3: Авторизация ${providerName} по коду устройства
-