feat: реальные аккаунты из 9Router — видно какой аккаунт исчерпан
This commit is contained in:
parent
89f7e8fef6
commit
759c1af29f
3 changed files with 120 additions and 19 deletions
72
backend/api/accounts.py
Normal file
72
backend/api/accounts.py
Normal 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"])}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue