feat(ui): show OpenClaw version tooltip on Gateway button

This commit is contained in:
xmanrui
2026-03-07 03:54:42 +08:00
parent 46e21f3a56
commit 1627aec9fb
2 changed files with 43 additions and 0 deletions
+27
View File
@@ -8,6 +8,7 @@ const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME ||
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
const DEGRADED_LATENCY_MS = 1500;
const execFileAsync = promisify(execFile);
let cachedOpenclawVersion: { value: string | null; expiresAt: number } | null = null;
function parseJsonFromMixedOutput(output: string): any {
for (let i = 0; i < output.length; i++) {
@@ -65,9 +66,29 @@ async function probeGatewayViaCli(token: string, timeoutMs = 5000): Promise<{ ok
}
}
async function getOpenclawVersion(): Promise<string | undefined> {
const now = Date.now();
if (cachedOpenclawVersion && cachedOpenclawVersion.expiresAt > now) {
return cachedOpenclawVersion.value || undefined;
}
try {
const { stdout } = await execFileAsync("openclaw", ["--version"], {
maxBuffer: 1024 * 1024,
env: { ...process.env, FORCE_COLOR: "0" },
});
const version = stdout.trim().split(/\s+/)[0] || null;
cachedOpenclawVersion = { value: version, expiresAt: now + 60 * 60 * 1000 };
return version || undefined;
} catch {
cachedOpenclawVersion = { value: null, expiresAt: now + 60 * 1000 };
return undefined;
}
}
export async function GET() {
const startedAt = Date.now();
try {
const openclawVersion = await getOpenclawVersion();
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
const config = JSON.parse(raw);
const port = config.gateway?.port || 18789;
@@ -89,6 +110,7 @@ export async function GET() {
return NextResponse.json({
ok: true,
data,
openclawVersion,
status: responseMs > DEGRADED_LATENCY_MS ? "degraded" : "healthy",
checkedAt,
responseMs,
@@ -104,6 +126,7 @@ export async function GET() {
return NextResponse.json({
ok: true,
data: null,
openclawVersion,
status: "healthy",
checkedAt,
responseMs,
@@ -113,12 +136,14 @@ export async function GET() {
return NextResponse.json({
ok: false,
openclawVersion,
error: cli.error || `HTTP ${resp.status}`,
status: "down",
checkedAt,
responseMs,
});
} catch (err: any) {
const openclawVersion = await getOpenclawVersion();
// If HTTP probe fails due transport/runtime issues, attempt CLI probe before declaring down.
const raw = err.cause?.code === "ECONNREFUSED"
? "Gateway 未运行"
@@ -150,6 +175,7 @@ export async function GET() {
return NextResponse.json({
ok: true,
data: null,
openclawVersion,
status: "healthy",
checkedAt,
responseMs,
@@ -158,6 +184,7 @@ export async function GET() {
}
return NextResponse.json({
ok: false,
openclawVersion,
error: cli.error || raw,
status: "down",
checkedAt,
+16
View File
@@ -17,6 +17,7 @@ interface HealthResult {
error?: string;
data?: any;
webUrl?: string;
openclawVersion?: string;
}
interface GatewayStatusProps {
@@ -29,6 +30,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
const { t } = useI18n();
const [health, setHealth] = useState<HealthResult | null>(null);
const [showError, setShowError] = useState(false);
const [showVersionTip, setShowVersionTip] = useState(false);
const check = useCallback(() => {
fetch("/api/gateway-health")
@@ -43,12 +45,21 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
return () => clearInterval(timer);
}, [check]);
const gatewayTitle = health?.openclawVersion
? `OpenClaw ${health.openclawVersion}`
: "OpenClaw";
return (
<div className={`relative inline-flex items-center gap-1.5 ${className}`.trim()}>
<a
href={health?.ok && health.webUrl ? resolveGatewayUrl(health.webUrl) : undefined}
target="_blank"
rel="noopener noreferrer"
title={gatewayTitle}
onMouseEnter={() => setShowVersionTip(true)}
onMouseLeave={() => setShowVersionTip(false)}
onFocus={() => setShowVersionTip(true)}
onBlur={() => setShowVersionTip(false)}
className={`inline-flex items-center rounded-full font-medium border hover:bg-cyan-500/30 transition-colors cursor-pointer ${
compact ? "px-2 py-1 text-[10px]" : "px-2 py-0.5 text-xs"
} ${
@@ -65,6 +76,11 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
) : "🦞 Gateway"}
<span className="opacity-50 text-[10px]"></span>
</a>
{showVersionTip && (
<div className="absolute top-full left-0 mt-1 z-50 px-2 py-1 rounded-md bg-black/80 border border-white/10 text-white text-[10px] whitespace-nowrap shadow-lg pointer-events-none">
{gatewayTitle}
</div>
)}
{!health ? (
<span className={compact ? "text-[10px] text-[var(--text-muted)]" : "text-xs text-[var(--text-muted)]"}>--</span>
) : health.ok ? (