feat: Gateway health monitoring (Hermes CLI, API, frontend)
- Hermes CLI wrapper using subprocess.run for gateway status - Gateway API: status list (with filter), events (placeholder), start/stop/restart with audit logging - Frontend GatewayPage: timeline, filter bar, status table with 10s polling, operation modals - usePolling hook for periodic data refresh Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""Gateway API — 状态、事件、启停操作。"""
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import require_auth, require_role
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.user import User
|
||||
from app.services import hermes_cli
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/gateway", tags=["gateway"])
|
||||
|
||||
# ── 占位事件数据(TODO: 从 hermes 日志或系统日志解析) ──
|
||||
|
||||
PLACEHOLDER_EVENTS = [
|
||||
{"time": "2026-07-22T18:45", "profile": "monitor", "type": "heartbeat_timeout",
|
||||
"detail": "连续 3 次心跳无响应"},
|
||||
{"time": "2026-07-22T14:20", "profile": "feishu-gw", "type": "feishu_disconnect",
|
||||
"detail": "WebSocket 断开 8 分钟"},
|
||||
{"time": "2026-07-22T10:30", "profile": "gateway-monitor", "type": "restart",
|
||||
"detail": "Gateway 重启"},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_gateway_status(
|
||||
status: str | None = Query(None, pattern="^(online|offline|warn)$"),
|
||||
_user: User = Depends(require_auth),
|
||||
):
|
||||
"""返回全部 Profile 的 Gateway 状态列表,支持 ?status= 筛选。"""
|
||||
gateways = hermes_cli.gateway_status_all()
|
||||
for gw in gateways:
|
||||
gw.setdefault("downtime_minutes", 0)
|
||||
gw.setdefault("restarts_today", 0)
|
||||
gw.setdefault("last_event", "")
|
||||
gw.setdefault("profile_type", "worker")
|
||||
if status:
|
||||
gateways = [g for g in gateways if g.get("status") == status]
|
||||
return gateways
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def get_gateway_events(
|
||||
_user: User = Depends(require_auth),
|
||||
):
|
||||
"""返回最近 24h 异常事件。"""
|
||||
# TODO: 从 hermes CLI 日志或系统日志解析真实事件
|
||||
return PLACEHOLDER_EVENTS
|
||||
|
||||
|
||||
async def _operate_gateway(
|
||||
profile: str,
|
||||
action: str,
|
||||
user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
"""执行 hermes gateway {action} --profile {profile} 并写入审计日志。"""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[settings.hermes_cli_path, "gateway", action, "--profile", profile],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
success = proc.returncode == 0
|
||||
detail = (proc.stdout or proc.stderr).strip()
|
||||
except Exception as e:
|
||||
success = False
|
||||
detail = str(e)
|
||||
|
||||
audit = AuditLog(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
action=f"gateway:{action}",
|
||||
target=f"profile:{profile}",
|
||||
detail=detail,
|
||||
)
|
||||
db.add(audit)
|
||||
await db.commit()
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=f"Gateway {action} 失败: {detail}")
|
||||
|
||||
return {"success": True, "action": action, "profile": profile, "detail": detail}
|
||||
|
||||
|
||||
@router.post("/{profile}/start")
|
||||
async def start_gateway(
|
||||
profile: str,
|
||||
user: User = Depends(require_role("owner", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""启动 Gateway。"""
|
||||
return await _operate_gateway(profile, "start", user, db)
|
||||
|
||||
|
||||
@router.post("/{profile}/stop")
|
||||
async def stop_gateway(
|
||||
profile: str,
|
||||
user: User = Depends(require_role("owner", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""停止 Gateway。"""
|
||||
return await _operate_gateway(profile, "stop", user, db)
|
||||
|
||||
|
||||
@router.post("/{profile}/restart")
|
||||
async def restart_gateway(
|
||||
profile: str,
|
||||
user: User = Depends(require_role("owner", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""重启 Gateway。"""
|
||||
return await _operate_gateway(profile, "restart", user, db)
|
||||
Reference in New Issue
Block a user