feat: реальные аккаунты из 9Router — видно какой аккаунт исчерпан

This commit is contained in:
ochenstarik-ui 2026-07-20 23:16:33 +07:00
parent 89f7e8fef6
commit 759c1af29f
3 changed files with 120 additions and 19 deletions

72
backend/api/accounts.py Normal file
View file

@ -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"])}

View file

@ -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")

View file

@ -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<string, any[]> = {}
accList.forEach((a: any) => {
if (!byProvider[a.provider]) byProvider[a.provider] = []
byProvider[a.provider].push(a)
})
const providerNames: Record<string, string> = {
'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
},