mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
feat(ui): show OpenClaw version tooltip on Gateway button
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user