From 759c1af29f1fe20d78ed912e1579ef9f8b4f64ce Mon Sep 17 00:00:00 2001 From: ochenstarik-ui Date: Mon, 20 Jul 2026 23:16:33 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B8=D0=B7=209Router=20=E2=80=94=20=D0=B2=D0=B8=D0=B4?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BA=D0=B0=D0=BA=D0=BE=D0=B9=20=D0=B0=D0=BA?= =?UTF-8?q?=D0=BA=D0=B0=D1=83=D0=BD=D1=82=20=D0=B8=D1=81=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=BF=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/accounts.py | 72 ++++++++++++++++++++ backend/main.py | 3 +- frontend/src/components/agents/AgentGrid.tsx | 64 ++++++++++++----- 3 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 backend/api/accounts.py diff --git a/backend/api/accounts.py b/backend/api/accounts.py new file mode 100644 index 0000000..855648b --- /dev/null +++ b/backend/api/accounts.py @@ -0,0 +1,72 @@ +"""API для получения данных об аккаунтах из 9Router""" +import sqlite3, json +from pathlib import Path +from fastapi import APIRouter + +router = APIRouter(prefix="/api/v1/accounts", tags=["accounts"]) + +DB_PATH = Path.home() / "AppData/Roaming/9router/db/data.sqlite" + +@router.get("") +def get_accounts(): + """Возвращает все аккаунты из 9Router с их статусом""" + if not DB_PATH.exists(): + return {"accounts": [], "error": "9Router DB not found"} + + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + rows = conn.execute( + "SELECT id, provider, authType, name, email, isActive, data, updatedAt " + "FROM providerConnections ORDER BY provider, priority" + ).fetchall() + conn.close() + + accounts = [] + for r in rows: + data = json.loads(r["data"]) if r["data"] else {} + error_code = data.get("errorCode") + accounts.append({ + "id": r["id"], + "provider": r["provider"], + "auth_type": r["authType"], + "name": r["name"], + "email": r["email"], + "active": bool(r["isActive"]), + "status": "error" if error_code else ("ok" if data.get("testStatus") != "unavailable" else "unavailable"), + "error_code": error_code, + "last_error": data.get("lastError", "")[:100] if data.get("lastError") else None, + "updated": r["updatedAt"], + }) + + return {"accounts": accounts} + +@router.get("/provider/{provider}") +def get_provider_accounts(provider: str): + """Аккаунты конкретного провайдера""" + if not DB_PATH.exists(): + return {"accounts": [], "error": "9Router DB not found"} + + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + rows = conn.execute( + "SELECT id, name, email, isActive, data FROM providerConnections " + "WHERE provider = ? ORDER BY priority", (provider,) + ).fetchall() + conn.close() + + accounts = [] + for r in rows: + data = json.loads(r["data"]) if r["data"] else {} + error_code = data.get("errorCode") + backoff = data.get("backoffLevel", 0) + accounts.append({ + "id": r["id"], + "name": r["name"], + "email": r["email"], + "active": bool(r["isActive"]), + "ok": not error_code and backoff == 0, + "error_code": error_code, + "backoff_level": backoff, + }) + + return {"provider": provider, "accounts": accounts, "total_ok": sum(1 for a in accounts if a["ok"])} diff --git a/backend/main.py b/backend/main.py index 9f816d1..a84d849 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,7 +8,7 @@ from pathlib import Path from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from api import runs, agents, memory, skills, connectors, tasks +from api import runs, agents, memory, skills, connectors, tasks, accounts import asyncio, json app = FastAPI( @@ -23,6 +23,7 @@ app.include_router(memory.router) app.include_router(skills.router) app.include_router(connectors.router) app.include_router(tasks.router) +app.include_router(accounts.router) # WebSocket для real-time обновлений @app.websocket("/ws") diff --git a/frontend/src/components/agents/AgentGrid.tsx b/frontend/src/components/agents/AgentGrid.tsx index a94a937..6ae8bd1 100644 --- a/frontend/src/components/agents/AgentGrid.tsx +++ b/frontend/src/components/agents/AgentGrid.tsx @@ -8,24 +8,52 @@ export function AgentGrid() { const { isLoading } = useQuery({ queryKey: ['agents'], queryFn: async () => { - const r = await fetch('/api/v1/agents/health') - const raw = await r.json() - const mapped = Object.entries(raw).map(([id, info]: [string, any], i: number) => ({ - id, - name: id.replace('worker-', ''), - status: info.healthy ? ('online' as const) : ('busy' as const), - runtime: info.model?.split('/')[0] || '?', - model: info.model?.split('/').pop() || '?', - healthy: info.healthy, - cpu: [31, 60, 18, 72][i] || 50, - activeRuns: [4, 0, 2, 1][i] || 0, - tokens_used: [45800, 124800, 22000, 89000][i] || 50000, - tokens_limit: 200000, - budget_used: [12.4, 42.5, 5.2, 28.0][i] || 20, - budget_limit: 100, - account: ['kimi-k2.7-main', 'kimi-k2.7-fast', 'gemini-3-flash', 'deepseek-v4-pro'][i] || '?', - provider: ['OpenCode Go', 'OpenCode Go', 'Google', 'NVIDIA'][i] || '?', - })) + const [healthR, accountsR] = await Promise.all([ + fetch('/api/v1/agents/health'), + fetch('/api/v1/accounts') + ]) + const raw = await healthR.json() + const accData = await accountsR.json() + const accList = accData.accounts || [] + + // Group accounts by provider + const byProvider: Record = {} + accList.forEach((a: any) => { + if (!byProvider[a.provider]) byProvider[a.provider] = [] + byProvider[a.provider].push(a) + }) + + const providerNames: Record = { + 'opencode-go': 'OpenCode Go', 'gemini': 'Google Gemini', + 'nvidia': 'NVIDIA', 'ollama': 'Ollama', 'cline': 'Cline', + } + + const mapped = Object.entries(raw).map(([id, info]: [string, any], i: number) => { + const prov = info.model?.split('/')[0] || 'opencode-go' + const provAccounts = byProvider[prov] || [] + const activeAccs = provAccounts.filter(a => a.ok) + const totalAccs = provAccounts.length + + return { + id, + name: id.replace('worker-', ''), + status: info.healthy ? ('online' as const) : ('busy' as const), + runtime: info.model?.split('/')[0] || '?', + model: info.model?.split('/').pop() || '?', + healthy: info.healthy, + cpu: [31, 60, 18, 72][i] || 50, + activeRuns: [4, 0, 2, 1][i] || 0, + tokens_used: activeAccs.length > 0 ? 45800 * activeAccs.length : 10000, + tokens_limit: 200000, + budget_used: [12.4, 42.5, 5.2, 28.0][i] || 20, + budget_limit: 100, + provider: providerNames[prov] || prov, + account: `${activeAccs.length}/${totalAccs} accounts`, + accounts_detail: provAccounts.map(a => ({ + name: a.name, ok: a.ok, error_code: a.error_code + })), + } + }) setAgents(mapped) return mapped },