mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
feat: add gateway health status indicator on bot dashboard
- Add /api/gateway-health endpoint to check gateway service status - Add GatewayStatus component with 10s auto-polling - Show green checkmark when healthy, red cross with error details when down - Click gateway badge to open OpenClaw web chat page - Fix variable name collision in alert-monitor.tsx (setInterval shadowing) - Add i18n translations for gateway status (zh/en)
This commit is contained in:
@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
|
||||
// 后台告警检查组件 - 服务启动时自动开始
|
||||
export function AlertMonitor() {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [interval, setInterval] = useState(10);
|
||||
const [checkInterval, setCheckInterval] = useState(10);
|
||||
const [lastResults, setLastResults] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
|
||||
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
||||
const config = JSON.parse(raw);
|
||||
const port = config.gateway?.port || 18789;
|
||||
const token = config.gateway?.auth?.token || "";
|
||||
|
||||
const url = `http://localhost:${port}/api/health`;
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const resp = await fetch(url, { headers, signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!resp.ok) {
|
||||
return NextResponse.json({ ok: false, error: `HTTP ${resp.status}` });
|
||||
}
|
||||
|
||||
const data = await resp.json().catch(() => null);
|
||||
return NextResponse.json({ ok: true, data, webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}` });
|
||||
} catch (err: any) {
|
||||
const msg = err.cause?.code === "ECONNREFUSED"
|
||||
? "Gateway 未运行"
|
||||
: err.name === "AbortError"
|
||||
? "请求超时"
|
||||
: err.message;
|
||||
return NextResponse.json({ ok: false, error: msg });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
|
||||
interface HealthResult {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
data?: any;
|
||||
webUrl?: string;
|
||||
}
|
||||
|
||||
export function GatewayStatus() {
|
||||
const { t } = useI18n();
|
||||
const [health, setHealth] = useState<HealthResult | null>(null);
|
||||
const [showError, setShowError] = useState(false);
|
||||
|
||||
const check = useCallback(() => {
|
||||
fetch("/api/gateway-health")
|
||||
.then((r) => r.json())
|
||||
.then((d) => setHealth(d))
|
||||
.catch(() => setHealth({ ok: false, error: t("gateway.fetchError") }));
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
check();
|
||||
const timer = setInterval(check, 10000);
|
||||
return () => clearInterval(timer);
|
||||
}, [check]);
|
||||
|
||||
if (!health) return null;
|
||||
|
||||
return (
|
||||
<div className="relative inline-flex items-center gap-2">
|
||||
<a
|
||||
href={health.ok && health.webUrl ? health.webUrl : undefined}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border bg-cyan-500/20 text-cyan-300 border-cyan-500/30 hover:bg-cyan-500/30 transition-colors cursor-pointer"
|
||||
>
|
||||
🌐 Gateway
|
||||
<span className="opacity-50 text-[10px]">↗</span>
|
||||
</a>
|
||||
{health.ok ? (
|
||||
<span className="text-green-400 text-sm cursor-help" title={t("gateway.healthy")}>✅</span>
|
||||
) : (
|
||||
<span
|
||||
className="text-red-400 text-sm cursor-pointer"
|
||||
title={health.error || t("gateway.unhealthy")}
|
||||
onClick={() => setShowError((v) => !v)}
|
||||
>❌</span>
|
||||
)}
|
||||
{showError && !health.ok && health.error && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 px-3 py-2 rounded-lg bg-red-500/15 border border-red-500/30 text-red-300 text-xs max-w-64 whitespace-pre-wrap shadow-lg">
|
||||
{health.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { GatewayStatus } from "./gateway-status";
|
||||
|
||||
interface Platform {
|
||||
name: string;
|
||||
@@ -611,6 +612,11 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gateway 状态 */}
|
||||
<div className="mb-4">
|
||||
<GatewayStatus />
|
||||
</div>
|
||||
|
||||
{/* 卡片墙 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{data.agents.map((agent) => (
|
||||
|
||||
@@ -184,6 +184,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"skills.noDesc": "无描述",
|
||||
"skills.source.builtin": "内置",
|
||||
"skills.source.custom": "自定义",
|
||||
|
||||
// gateway status
|
||||
"gateway.healthy": "Gateway 运行正常",
|
||||
"gateway.unhealthy": "Gateway 异常",
|
||||
"gateway.fetchError": "无法检查 Gateway 状态",
|
||||
},
|
||||
en: {
|
||||
// layout
|
||||
@@ -364,6 +369,11 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"skills.noDesc": "No description",
|
||||
"skills.source.builtin": "Built-in",
|
||||
"skills.source.custom": "Custom",
|
||||
|
||||
// gateway status
|
||||
"gateway.healthy": "Gateway is running",
|
||||
"gateway.unhealthy": "Gateway is down",
|
||||
"gateway.fetchError": "Cannot check Gateway status",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user