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 import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
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
|
import asyncio, json
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
|
@ -23,6 +23,7 @@ app.include_router(memory.router)
|
||||||
app.include_router(skills.router)
|
app.include_router(skills.router)
|
||||||
app.include_router(connectors.router)
|
app.include_router(connectors.router)
|
||||||
app.include_router(tasks.router)
|
app.include_router(tasks.router)
|
||||||
|
app.include_router(accounts.router)
|
||||||
|
|
||||||
# WebSocket для real-time обновлений
|
# WebSocket для real-time обновлений
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,52 @@ export function AgentGrid() {
|
||||||
const { isLoading } = useQuery({
|
const { isLoading } = useQuery({
|
||||||
queryKey: ['agents'],
|
queryKey: ['agents'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const r = await fetch('/api/v1/agents/health')
|
const [healthR, accountsR] = await Promise.all([
|
||||||
const raw = await r.json()
|
fetch('/api/v1/agents/health'),
|
||||||
const mapped = Object.entries(raw).map(([id, info]: [string, any], i: number) => ({
|
fetch('/api/v1/accounts')
|
||||||
id,
|
])
|
||||||
name: id.replace('worker-', ''),
|
const raw = await healthR.json()
|
||||||
status: info.healthy ? ('online' as const) : ('busy' as const),
|
const accData = await accountsR.json()
|
||||||
runtime: info.model?.split('/')[0] || '?',
|
const accList = accData.accounts || []
|
||||||
model: info.model?.split('/').pop() || '?',
|
|
||||||
healthy: info.healthy,
|
// Group accounts by provider
|
||||||
cpu: [31, 60, 18, 72][i] || 50,
|
const byProvider: Record<string, any[]> = {}
|
||||||
activeRuns: [4, 0, 2, 1][i] || 0,
|
accList.forEach((a: any) => {
|
||||||
tokens_used: [45800, 124800, 22000, 89000][i] || 50000,
|
if (!byProvider[a.provider]) byProvider[a.provider] = []
|
||||||
tokens_limit: 200000,
|
byProvider[a.provider].push(a)
|
||||||
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] || '?',
|
const providerNames: Record<string, string> = {
|
||||||
provider: ['OpenCode Go', 'OpenCode Go', 'Google', 'NVIDIA'][i] || '?',
|
'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)
|
setAgents(mapped)
|
||||||
return mapped
|
return mapped
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue