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:
enzo
2026-07-22 17:05:41 +08:00
co-authored by Claude
parent 76045538bb
commit 6d4a5c955b
6 changed files with 553 additions and 3 deletions
+120
View File
@@ -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)
+2 -1
View File
@@ -4,7 +4,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.database import engine, Base
from app.api import auth, users, audit, dashboard
from app.api import auth, users, audit, dashboard, gateway
from app.middleware.audit import AuditMiddleware
@@ -35,6 +35,7 @@ app.include_router(auth.router)
app.include_router(users.router)
app.include_router(audit.router)
app.include_router(dashboard.router, prefix="/api/v1")
app.include_router(gateway.router, prefix="/api/v1")
@app.get("/api/v1/health")
+43
View File
@@ -0,0 +1,43 @@
"""Hermes CLI 封装 — 通过 subprocess 调用 hermes 命令。"""
import json
import logging
import subprocess
from app.config import settings
logger = logging.getLogger(__name__)
def _run_hermes(*args: str, timeout: int = 15) -> subprocess.CompletedProcess:
"""执行 hermes CLI 命令,返回 CompletedProcess。"""
return subprocess.run(
[settings.hermes_cli_path, *args],
capture_output=True,
text=True,
timeout=timeout,
)
def gateway_status_all() -> list[dict]:
"""执行 `hermes gateway status`,返回解析后的 Gateway 状态列表。
出错时返回空列表并记录告警日志。
"""
try:
proc = _run_hermes("gateway", "status")
if proc.returncode != 0:
logger.warning("hermes gateway status 返回非零: %s", proc.stderr.strip())
return []
data = json.loads(proc.stdout)
if isinstance(data, list):
return data
return data.get("gateways", [])
except FileNotFoundError:
logger.warning("Hermes CLI 未找到: %s", settings.hermes_cli_path)
except json.JSONDecodeError as e:
logger.warning("解析 hermes gateway status 输出失败: %s", e)
except subprocess.TimeoutExpired:
logger.warning("hermes gateway status 执行超时")
except Exception as e:
logger.warning("调用 hermes gateway status 异常: %s", e)
return []