fix: repair account setup and routing actions
This commit is contained in:
parent
4c45a1c73c
commit
f0461bc539
5 changed files with 184 additions and 29 deletions
|
|
@ -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]:
|
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()
|
config = load_router_config()
|
||||||
pcfg = config.get_profile(profile_id)
|
pcfg = config.get_profile(profile_id)
|
||||||
if not pcfg:
|
if not pcfg:
|
||||||
|
|
@ -119,22 +119,30 @@ def do_test_profile(provider: str, profile_id: str) -> Dict[str, Any]:
|
||||||
if not status.get("authenticated"):
|
if not status.get("authenticated"):
|
||||||
return {"success": False, "error": "Аккаунт не добавлен. Сначала выполните подключение."}
|
return {"success": False, "error": "Аккаунт не добавлен. Сначала выполните подключение."}
|
||||||
|
|
||||||
adapter = get_adapter(pcfg.provider)
|
|
||||||
model = pcfg.preferred_models[0] if pcfg.preferred_models else "default"
|
model = pcfg.preferred_models[0] if pcfg.preferred_models else "default"
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
try:
|
try:
|
||||||
resp = adapter.invoke(
|
auth_data = ProfileAuthManager.load_profile_auth(pcfg.provider, profile_id)
|
||||||
pcfg,
|
if not auth_data:
|
||||||
{
|
return {"success": False, "error": "Сохранённые данные авторизации не найдены"}
|
||||||
"model": model,
|
adapter = get_adapter(pcfg.provider)
|
||||||
"messages": [{"role": "user", "content": f"Respond strictly with: TEST_OK_FOR_{profile_id}"}],
|
runtime_ready = adapter.health_check(pcfg)
|
||||||
"temperature": 0.1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
el = round(time.time() - t0, 2)
|
el = round(time.time() - t0, 2)
|
||||||
content = resp.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
|
if not runtime_ready:
|
||||||
EventLogService.get().log("system", f"Тест {profile_id} ({model}) успешно пройден за {el}s.", level="success")
|
return {
|
||||||
return {"success": True, "model": model, "duration_sec": el, "response": content[:120]}
|
"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:
|
except Exception as e:
|
||||||
EventLogService.get().log("system", f"Ошибка теста {profile_id} ({model}): {e}", level="error")
|
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)}
|
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),
|
on_complete=lambda: self.after(0, self._refresh_data),
|
||||||
)
|
)
|
||||||
elif action == "edit_route":
|
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":
|
elif action == "open_routing":
|
||||||
self._show_view("routing")
|
self._show_view("routing")
|
||||||
routing = self._views.get("routing")
|
routing = self._views.get("routing")
|
||||||
|
|
@ -672,14 +685,19 @@ class HermesHubApp(ctk.CTk):
|
||||||
justify="left",
|
justify="left",
|
||||||
).pack(anchor="w", pady=(0, 12))
|
).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 = [
|
roles = [
|
||||||
("orchestrator", "👑 Главный оркестратор"),
|
("orchestrator", "👑 Главный оркестратор"),
|
||||||
("coder", "💻 Кодер (Code Generation)"),
|
("coder-primary", "💻 Основной кодер"),
|
||||||
|
("coder-secondary", "💻 Резервный кодер"),
|
||||||
("reviewer", "🔍 Ревьюер (Code Review)"),
|
("reviewer", "🔍 Ревьюер (Code Review)"),
|
||||||
("researcher", "🌐 Исследователь (Search / Docs)"),
|
("research", "🌐 Исследователь (Search / Docs)"),
|
||||||
("tester", "🧪 Тестировщик (Deterministic Tests)"),
|
("fast", "⚡ Быстрый агент"),
|
||||||
("general", "⚡ Агент общего назначения (Subagent)"),
|
|
||||||
("spare", "🛡️ Резерв (Spare)"),
|
("spare", "🛡️ Резерв (Spare)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -695,9 +713,24 @@ class HermesHubApp(ctk.CTk):
|
||||||
hover_color=Theme.ACCENT_HOVER,
|
hover_color=Theme.ACCENT_HOVER,
|
||||||
).pack(anchor="w", padx=8, pady=3)
|
).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():
|
def _save():
|
||||||
chosen = role_var.get()
|
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()
|
modal.destroy()
|
||||||
self._show_toast(f"✅ {msg}" if ok else f"❌ {msg}")
|
self._show_toast(f"✅ {msg}" if ok else f"❌ {msg}")
|
||||||
self._refresh_data()
|
self._refresh_data()
|
||||||
|
|
@ -714,7 +747,7 @@ class HermesHubApp(ctk.CTk):
|
||||||
|
|
||||||
def _show_test_result(self, result: Dict[str, Any]):
|
def _show_test_result(self, result: Dict[str, Any]):
|
||||||
if result.get("success"):
|
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:
|
else:
|
||||||
msg = f"✕ Ошибка теста: {result.get('error', 'Неизвестная ошибка')}"
|
msg = f"✕ Ошибка теста: {result.get('error', 'Неизвестная ошибка')}"
|
||||||
self._show_toast(msg)
|
self._show_toast(msg)
|
||||||
|
|
|
||||||
|
|
@ -482,6 +482,18 @@ class AddAccountWizard(HubModal):
|
||||||
auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
|
auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
|
||||||
auth_card.pack(fill="x", pady=(0, 8))
|
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(
|
ctk.CTkLabel(
|
||||||
auth_card,
|
auth_card,
|
||||||
text="Ссылка для входа в OpenAI (ChatGPT):",
|
text="Ссылка для входа в OpenAI (ChatGPT):",
|
||||||
|
|
@ -655,6 +667,14 @@ class AddAccountWizard(HubModal):
|
||||||
|
|
||||||
def _open_codex_browser(self):
|
def _open_codex_browser(self):
|
||||||
if self.codex_url:
|
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)
|
webbrowser.open(self.codex_url)
|
||||||
|
|
||||||
def _handle_codex_manual_submit(self):
|
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 = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
|
||||||
auth_card.pack(fill="x", pady=(0, 8))
|
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(
|
ctk.CTkLabel(
|
||||||
auth_card,
|
auth_card,
|
||||||
text="Ссылка для входа в xAI Grok:",
|
text="Ссылка для входа в xAI Grok:",
|
||||||
|
|
@ -1091,6 +1123,14 @@ class AddAccountWizard(HubModal):
|
||||||
|
|
||||||
def _open_grok_browser(self):
|
def _open_grok_browser(self):
|
||||||
if self.grok_url:
|
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)
|
webbrowser.open(self.grok_url)
|
||||||
|
|
||||||
def _handle_grok_manual_submit(self):
|
def _handle_grok_manual_submit(self):
|
||||||
|
|
@ -1455,6 +1495,15 @@ class AddAccountWizard(HubModal):
|
||||||
anchor="w",
|
anchor="w",
|
||||||
).pack(fill="x", pady=8)
|
).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(
|
HubButton(
|
||||||
self.footer,
|
self.footer,
|
||||||
text="✓ Завершить подключение",
|
text="✓ Завершить подключение",
|
||||||
|
|
@ -1466,13 +1515,19 @@ class AddAccountWizard(HubModal):
|
||||||
# Most built-in slots already occur in a default chain. Custom or
|
# Most built-in slots already occur in a default chain. Custom or
|
||||||
# repaired configs may not, so completion makes that invariant explicit
|
# repaired configs may not, so completion makes that invariant explicit
|
||||||
# without reordering a slot that is already assigned.
|
# without reordering a slot that is already assigned.
|
||||||
ensure_profile_in_routing(self.target_slot)
|
ok, message = ensure_profile_in_routing(self.target_slot)
|
||||||
EventLogService.get().log_event(
|
if not ok:
|
||||||
event_type="ACCOUNT_CONNECTED",
|
self.finish_status_lbl.configure(text=f"❌ {message}", text_color=Theme.STATUS_ERROR)
|
||||||
title=f"Подключен аккаунт {self.selected_provider}",
|
return
|
||||||
detail=f"Слот: {self.target_slot} ({self.discovered_identity})",
|
# A reused slot can carry cooldown from an older account. Fresh OAuth
|
||||||
provider=self.selected_provider,
|
# credentials must start with fresh health state.
|
||||||
profile_id=self.target_slot,
|
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:
|
if self.on_complete:
|
||||||
self.on_complete(
|
self.on_complete(
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,8 @@ class HubModal(ctk.CTkToplevel):
|
||||||
self,
|
self,
|
||||||
corner_radius=Theme.RADIUS_LG,
|
corner_radius=Theme.RADIUS_LG,
|
||||||
border_color=Theme.BORDER_ACCENT,
|
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)
|
self.container.pack(fill="both", expand=True, padx=Theme.SPACE_LG, pady=Theme.SPACE_LG)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -712,6 +712,18 @@ class TeamView(ctk.CTkFrame):
|
||||||
break
|
break
|
||||||
return "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:
|
def _connect_selected(self) -> None:
|
||||||
if len(self.controller.graph.nodes) < 2:
|
if len(self.controller.graph.nodes) < 2:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pytest.importorskip("customtkinter")
|
||||||
|
|
||||||
from antigravity_provider.router.ui import routing_graph as graph_module
|
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.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 (
|
from antigravity_provider.router.ui.routing_graph import (
|
||||||
GraphEdge,
|
GraphEdge,
|
||||||
GraphNode,
|
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 wizard_module.ensure_profile_in_routing("new-slot")[0]
|
||||||
assert calls == [("new-slot", "coder", False)]
|
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",)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue