From f0461bc5391acebd6e79bca4c5090a0765478a1b Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Sat, 22 Aug 2026 09:40:14 +0700 Subject: [PATCH] fix: repair account setup and routing actions --- .../router/hermes_hub_app.py | 75 +++++++++++++------ .../router/ui/add_account_wizard.py | 69 +++++++++++++++-- .../router/ui/components.py | 3 +- .../router/ui/views/team_view.py | 12 +++ tests/test_ui_routing_graph.py | 54 +++++++++++++ 5 files changed, 184 insertions(+), 29 deletions(-) diff --git a/src/antigravity_provider/router/hermes_hub_app.py b/src/antigravity_provider/router/hermes_hub_app.py index c48b6cd..020dd9d 100644 --- a/src/antigravity_provider/router/hermes_hub_app.py +++ b/src/antigravity_provider/router/hermes_hub_app.py @@ -109,7 +109,7 @@ def do_set_orchestrator(profile_id: str) -> Tuple[bool, str]: def do_test_profile(provider: str, profile_id: str) -> Dict[str, Any]: - """Strictly tests stored credentials WITHOUT triggering OAuth or opening browsers.""" + """Check local profile readiness without inference, OAuth, or a browser.""" config = load_router_config() pcfg = config.get_profile(profile_id) if not pcfg: @@ -119,22 +119,30 @@ def do_test_profile(provider: str, profile_id: str) -> Dict[str, Any]: if not status.get("authenticated"): return {"success": False, "error": "Аккаунт не добавлен. Сначала выполните подключение."} - adapter = get_adapter(pcfg.provider) model = pcfg.preferred_models[0] if pcfg.preferred_models else "default" t0 = time.time() try: - resp = adapter.invoke( - pcfg, - { - "model": model, - "messages": [{"role": "user", "content": f"Respond strictly with: TEST_OK_FOR_{profile_id}"}], - "temperature": 0.1, - }, - ) + auth_data = ProfileAuthManager.load_profile_auth(pcfg.provider, profile_id) + if not auth_data: + return {"success": False, "error": "Сохранённые данные авторизации не найдены"} + adapter = get_adapter(pcfg.provider) + runtime_ready = adapter.health_check(pcfg) el = round(time.time() - t0, 2) - content = resp.get("choices", [{}])[0].get("message", {}).get("content", "").strip() - EventLogService.get().log("system", f"Тест {profile_id} ({model}) успешно пройден за {el}s.", level="success") - return {"success": True, "model": model, "duration_sec": el, "response": content[:120]} + if not runtime_ready: + return { + "success": False, + "duration_sec": el, + "error": "Локальный runtime провайдера недоступен; повторная авторизация не запускалась", + } + EventLogService.get().log( + "system", f"Локальная проверка профиля {profile_id} ({model}) пройдена за {el}s.", level="success" + ) + return { + "success": True, + "model": model, + "duration_sec": el, + "response": "Авторизация сохранена; runtime провайдера доступен", + } except Exception as e: EventLogService.get().log("system", f"Ошибка теста {profile_id} ({model}): {e}", level="error") return {"success": False, "model": model, "duration_sec": round(time.time() - t0, 2), "error": str(e)} @@ -630,7 +638,12 @@ class HermesHubApp(ctk.CTk): on_complete=lambda: self.after(0, self._refresh_data), ) elif action == "edit_route": - self._show_toast("Редактор цепочки использует кнопки и селекторы; drag-and-drop отключён.") + role_id = data.get("role_id", "") + self._show_view("team") + team = self._views.get("team") + if team and hasattr(team, "focus_role"): + team.focus_role(role_id) + self._show_toast(f"Настройка цепочки роли: {role_id}") elif action == "open_routing": self._show_view("routing") routing = self._views.get("routing") @@ -672,14 +685,19 @@ class HermesHubApp(ctk.CTk): justify="left", ).pack(anchor="w", pady=(0, 12)) - role_var = ctk.StringVar(value="orchestrator") + config = load_router_config() + current_role = next( + (role_id for role_id, policy in config.roles.items() if profile_id in policy.preferred_chain), + "orchestrator", + ) + role_var = ctk.StringVar(value=current_role) roles = [ ("orchestrator", "👑 Главный оркестратор"), - ("coder", "💻 Кодер (Code Generation)"), + ("coder-primary", "💻 Основной кодер"), + ("coder-secondary", "💻 Резервный кодер"), ("reviewer", "🔍 Ревьюер (Code Review)"), - ("researcher", "🌐 Исследователь (Search / Docs)"), - ("tester", "🧪 Тестировщик (Deterministic Tests)"), - ("general", "⚡ Агент общего назначения (Subagent)"), + ("research", "🌐 Исследователь (Search / Docs)"), + ("fast", "⚡ Быстрый агент"), ("spare", "🛡️ Резерв (Spare)"), ] @@ -695,9 +713,24 @@ class HermesHubApp(ctk.CTk): hover_color=Theme.ACCENT_HOVER, ).pack(anchor="w", padx=8, pady=3) + primary_var = ctk.BooleanVar(value=True) + ctk.CTkCheckBox( + modal.body, + text="Сделать основным в выбранной цепочке", + variable=primary_var, + font=Theme.font_body(), + text_color=Theme.TEXT_PRIMARY, + fg_color=Theme.ACCENT, + hover_color=Theme.ACCENT_HOVER, + ).pack(anchor="w", padx=8, pady=(12, 3)) + def _save(): chosen = role_var.get() - ok, msg = AutoAssigner.assign_profile_to_role(profile_id, chosen, is_primary=(chosen != "spare")) + ok, msg = AutoAssigner.assign_profile_to_role( + profile_id, + chosen, + is_primary=primary_var.get() and chosen != "spare", + ) modal.destroy() self._show_toast(f"✅ {msg}" if ok else f"❌ {msg}") self._refresh_data() @@ -714,7 +747,7 @@ class HermesHubApp(ctk.CTk): def _show_test_result(self, result: Dict[str, Any]): if result.get("success"): - msg = f"✓ Тест успешен | Модель: {result.get('model')} | Время: {result.get('duration_sec')}s" + msg = f"✓ Профиль готов | Модель: {result.get('model')} | Время: {result.get('duration_sec')}s" else: msg = f"✕ Ошибка теста: {result.get('error', 'Неизвестная ошибка')}" self._show_toast(msg) diff --git a/src/antigravity_provider/router/ui/add_account_wizard.py b/src/antigravity_provider/router/ui/add_account_wizard.py index a449d15..45876f6 100644 --- a/src/antigravity_provider/router/ui/add_account_wizard.py +++ b/src/antigravity_provider/router/ui/add_account_wizard.py @@ -482,6 +482,18 @@ class AddAccountWizard(HubModal): auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED) auth_card.pack(fill="x", pady=(0, 8)) + ctk.CTkLabel( + auth_card, + text=( + "1. Нажмите «Открыть в браузере». 2. Вставьте показанный ниже код на странице OpenAI. " + "3. Вернитесь в Hub — поле для этого кода в приложении не требуется." + ), + font=Theme.font_caption(), + text_color=Theme.TEXT_SECONDARY, + wraplength=520, + justify="left", + ).pack(anchor="w", padx=10, pady=(7, 2)) + ctk.CTkLabel( auth_card, text="Ссылка для входа в OpenAI (ChatGPT):", @@ -655,6 +667,14 @@ class AddAccountWizard(HubModal): def _open_codex_browser(self): if self.codex_url: + self._copy_codex_code() + self.codex_status_lbl.configure( + text=( + "Код скопирован. Вставьте его на открывшейся странице OpenAI; " + "затем вернитесь сюда — Hub продолжит автоматически." + ), + text_color=Theme.TEXT_SECONDARY, + ) webbrowser.open(self.codex_url) def _handle_codex_manual_submit(self): @@ -918,6 +938,18 @@ class AddAccountWizard(HubModal): auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED) auth_card.pack(fill="x", pady=(0, 8)) + ctk.CTkLabel( + auth_card, + text=( + "1. Нажмите «Открыть в браузере». 2. Вставьте показанный ниже код на странице xAI. " + "3. Вернитесь в Hub — поле для этого кода в приложении не требуется." + ), + font=Theme.font_caption(), + text_color=Theme.TEXT_SECONDARY, + wraplength=520, + justify="left", + ).pack(anchor="w", padx=10, pady=(7, 2)) + ctk.CTkLabel( auth_card, text="Ссылка для входа в xAI Grok:", @@ -1091,6 +1123,14 @@ class AddAccountWizard(HubModal): def _open_grok_browser(self): if self.grok_url: + self._copy_grok_code() + self.grok_status_lbl.configure( + text=( + "Код скопирован. Вставьте его на открывшейся странице xAI; " + "затем вернитесь сюда — Hub продолжит автоматически." + ), + text_color=Theme.TEXT_SECONDARY, + ) webbrowser.open(self.grok_url) def _handle_grok_manual_submit(self): @@ -1455,6 +1495,15 @@ class AddAccountWizard(HubModal): anchor="w", ).pack(fill="x", pady=8) + self.finish_status_lbl = ctk.CTkLabel( + self.body, + text="Нажмите «Завершить подключение», чтобы сохранить роль и обновить Hub.", + font=Theme.font_caption(), + text_color=Theme.TEXT_SECONDARY, + anchor="w", + ) + self.finish_status_lbl.pack(fill="x", pady=(0, 4)) + HubButton( self.footer, text="✓ Завершить подключение", @@ -1466,13 +1515,19 @@ class AddAccountWizard(HubModal): # Most built-in slots already occur in a default chain. Custom or # repaired configs may not, so completion makes that invariant explicit # without reordering a slot that is already assigned. - ensure_profile_in_routing(self.target_slot) - EventLogService.get().log_event( - event_type="ACCOUNT_CONNECTED", - title=f"Подключен аккаунт {self.selected_provider}", - detail=f"Слот: {self.target_slot} ({self.discovered_identity})", - provider=self.selected_provider, - profile_id=self.target_slot, + ok, message = ensure_profile_in_routing(self.target_slot) + if not ok: + self.finish_status_lbl.configure(text=f"❌ {message}", text_color=Theme.STATUS_ERROR) + return + # A reused slot can carry cooldown from an older account. Fresh OAuth + # credentials must start with fresh health state. + from antigravity_provider.router.router_engine import get_router_engine + + get_router_engine().health.clear_cooldown(self.target_slot) + EventLogService.get().log( + "account", + f"Подключен аккаунт {self.selected_provider}; слот {self.target_slot}; роль сохранена.", + level="success", ) if self.on_complete: self.on_complete( diff --git a/src/antigravity_provider/router/ui/components.py b/src/antigravity_provider/router/ui/components.py index 418865f..3f4f208 100644 --- a/src/antigravity_provider/router/ui/components.py +++ b/src/antigravity_provider/router/ui/components.py @@ -481,7 +481,8 @@ class HubModal(ctk.CTkToplevel): self, corner_radius=Theme.RADIUS_LG, border_color=Theme.BORDER_ACCENT, - fg_color=Theme.DARK, + # Contrast-safe in dark, hybrid and light palettes. + fg_color=Theme.SURFACE, ) self.container.pack(fill="both", expand=True, padx=Theme.SPACE_LG, pady=Theme.SPACE_LG) diff --git a/src/antigravity_provider/router/ui/views/team_view.py b/src/antigravity_provider/router/ui/views/team_view.py index 969f7c5..0ae61a8 100644 --- a/src/antigravity_provider/router/ui/views/team_view.py +++ b/src/antigravity_provider/router/ui/views/team_view.py @@ -712,6 +712,18 @@ class TeamView(ctk.CTkFrame): break return "break" + def focus_role(self, role_id: str) -> None: + """Select a role when Routing delegates editing to this single editor.""" + if not any(node.role_id == role_id for node in self.controller.graph.nodes): + self.state_label.configure(text=f"Роль {role_id} не найдена", text_color=Theme.STATUS_WARNING) + return + self.selected_role = role_id + self.selected_edge = "" + target = next((node.role_id for node in self.controller.graph.nodes if node.role_id != role_id), role_id) + self.edge_target.set(target) + self._draw_graph(rebuild=True) + self.state_label.configure(text=f"Редактируется цепочка: {role_id}", text_color=Theme.TEXT_ACCENT) + def _connect_selected(self) -> None: if len(self.controller.graph.nodes) < 2: return diff --git a/tests/test_ui_routing_graph.py b/tests/test_ui_routing_graph.py index d2e7d3d..7cfdfcf 100644 --- a/tests/test_ui_routing_graph.py +++ b/tests/test_ui_routing_graph.py @@ -8,6 +8,7 @@ pytest.importorskip("customtkinter") from antigravity_provider.router.ui import routing_graph as graph_module from antigravity_provider.router.ui import add_account_wizard as wizard_module +from antigravity_provider.router import hermes_hub_app as app_module from antigravity_provider.router.ui.routing_graph import ( GraphEdge, GraphNode, @@ -128,3 +129,56 @@ def test_wizard_keeps_existing_chain_rank_and_assigns_missing_slot(monkeypatch): ) assert wizard_module.ensure_profile_in_routing("new-slot")[0] assert calls == [("new-slot", "coder", False)] + + +def test_profile_test_does_not_invoke_model_or_oauth(monkeypatch): + profile = SimpleNamespace(provider="antigravity", preferred_models=["gemini"], profile_id="connected") + config = SimpleNamespace(get_profile=lambda _profile_id: profile) + + class Adapter: + @staticmethod + def health_check(_profile): + return True + + @staticmethod + def invoke(*_args, **_kwargs): + raise AssertionError("profile test must never invoke inference") + + monkeypatch.setattr(app_module, "load_router_config", lambda: config) + monkeypatch.setattr(app_module.ProfileAuthManager, "get_profile_status", lambda *_args: {"authenticated": True}) + monkeypatch.setattr(app_module.ProfileAuthManager, "load_profile_auth", lambda *_args: {"token": "present"}) + monkeypatch.setattr(app_module, "get_adapter", lambda _provider: Adapter()) + monkeypatch.setattr(app_module.EventLogService, "get", lambda: SimpleNamespace(log=lambda *_args, **_kwargs: None)) + + result = app_module.do_test_profile("antigravity", "connected") + assert result["success"] is True + assert "runtime" in result["response"] + + +def test_wizard_finish_closes_logs_and_clears_reused_slot(monkeypatch): + calls = [] + monkeypatch.setattr(wizard_module, "ensure_profile_in_routing", lambda _profile: (True, "ok")) + monkeypatch.setattr( + wizard_module.EventLogService, + "get", + lambda: SimpleNamespace(log=lambda *args, **kwargs: calls.append(("log", args, kwargs))), + ) + from antigravity_provider.router import router_engine + + monkeypatch.setattr( + router_engine, + "get_router_engine", + lambda: SimpleNamespace(health=SimpleNamespace(clear_cooldown=lambda profile: calls.append(("clear", profile)))), + ) + fake = SimpleNamespace( + target_slot="ag-orch-fallback", + selected_provider="antigravity", + discovered_identity="account", + finish_status_lbl=SimpleNamespace(configure=lambda **_kwargs: None), + on_complete=lambda payload: calls.append(("complete", payload)), + destroy=lambda: calls.append(("destroy",)), + ) + wizard_module.AddAccountWizard._finish(fake) + assert ("clear", "ag-orch-fallback") in calls + assert any(item[0] == "log" for item in calls) + assert calls[-1] == ("destroy",)