From 53e5d5300005502efedfa07449bae4d2aef765c2 Mon Sep 17 00:00:00 2001 From: xmanrui <841206367@qq.com> Date: Sat, 7 Mar 2026 01:34:38 +0800 Subject: [PATCH] fix(gateway): fallback health check to CLI for OpenClaw 2026.3 --- app/api/gateway-health/route.ts | 143 +++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 20 deletions(-) diff --git a/app/api/gateway-health/route.ts b/app/api/gateway-health/route.ts index dfc4cc3..8c3bb01 100644 --- a/app/api/gateway-health/route.ts +++ b/app/api/gateway-health/route.ts @@ -1,10 +1,69 @@ import { NextResponse } from "next/server"; import fs from "fs"; import path from "path"; +import { execFile } from "child_process"; +import { promisify } from "util"; const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw"); const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json"); const DEGRADED_LATENCY_MS = 1500; +const execFileAsync = promisify(execFile); + +function parseJsonFromMixedOutput(output: string): any { + for (let i = 0; i < output.length; i++) { + if (output[i] !== "{") continue; + let depth = 0; + let inString = false; + let escaped = false; + for (let j = i; j < output.length; j++) { + const ch = output[j]; + if (inString) { + if (escaped) escaped = false; + else if (ch === "\\") escaped = true; + else if (ch === "\"") inString = false; + continue; + } + if (ch === "\"") { + inString = true; + continue; + } + if (ch === "{") depth++; + else if (ch === "}") { + depth--; + if (depth === 0) { + const candidate = output.slice(i, j + 1).trim(); + try { + return JSON.parse(candidate); + } catch { + break; + } + } + } + } + } + throw new Error("Failed to parse JSON from openclaw gateway status output"); +} + +async function probeGatewayViaCli(token: string, timeoutMs = 5000): Promise<{ ok: boolean; error?: string }> { + try { + const args = ["gateway", "status", "--json", "--timeout", String(timeoutMs)]; + if (token) args.push("--token", token); + const { stdout, stderr } = await execFileAsync("openclaw", args, { + maxBuffer: 10 * 1024 * 1024, + env: { ...process.env, FORCE_COLOR: "0" }, + }); + const parsed = parseJsonFromMixedOutput(`${stdout}\n${stderr || ""}`); + const ok = parsed?.rpc?.ok === true; + const error = + typeof parsed?.rpc?.error === "string" && parsed.rpc.error.trim() + ? parsed.rpc.error.trim() + : undefined; + return { ok, error }; + } catch (err: any) { + const message = err?.message || "Failed to run openclaw gateway status"; + return { ok: false, error: message }; + } +} export async function GET() { const startedAt = Date.now(); @@ -21,41 +80,85 @@ export async function GET() { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); - const resp = await fetch(url, { headers, signal: controller.signal }); + const resp = await fetch(url, { headers, signal: controller.signal, cache: "no-store" }); clearTimeout(timeout); - const checkedAt = Date.now(); - const responseMs = checkedAt - startedAt; - - if (!resp.ok) { + if (resp.ok) { + const checkedAt = Date.now(); + const responseMs = checkedAt - startedAt; + const data = await resp.json().catch(() => null); return NextResponse.json({ - ok: false, - error: `HTTP ${resp.status}`, - status: "down", + ok: true, + data, + status: responseMs > DEGRADED_LATENCY_MS ? "degraded" : "healthy", checkedAt, responseMs, + webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`, }); } - const data = await resp.json().catch(() => null); - return NextResponse.json({ - ok: true, - data, - status: responseMs > DEGRADED_LATENCY_MS ? "degraded" : "healthy", - checkedAt, - responseMs, - webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`, - }); - } catch (err: any) { + // OpenClaw 2026.3.x no longer serves /api/health; fallback to CLI probe. + const cli = await probeGatewayViaCli(token, 5000); const checkedAt = Date.now(); const responseMs = checkedAt - startedAt; - const msg = err.cause?.code === "ECONNREFUSED" + if (cli.ok) { + return NextResponse.json({ + ok: true, + data: null, + status: "healthy", + checkedAt, + responseMs, + webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`, + }); + } + + return NextResponse.json({ + ok: false, + error: cli.error || `HTTP ${resp.status}`, + status: "down", + checkedAt, + responseMs, + }); + } catch (err: any) { + // If HTTP probe fails due transport/runtime issues, attempt CLI probe before declaring down. + const raw = err.cause?.code === "ECONNREFUSED" ? "Gateway 未运行" : err.name === "AbortError" ? "请求超时" : err.message; + const token = (() => { + try { + const rawCfg = fs.readFileSync(CONFIG_PATH, "utf-8"); + const cfg = JSON.parse(rawCfg); + return cfg.gateway?.auth?.token || ""; + } catch { + return ""; + } + })(); + const cli = await probeGatewayViaCli(token, 5000); + const checkedAt = Date.now(); + const responseMs = checkedAt - startedAt; + if (cli.ok) { + const port = (() => { + try { + const rawCfg = fs.readFileSync(CONFIG_PATH, "utf-8"); + const cfg = JSON.parse(rawCfg); + return cfg.gateway?.port || 18789; + } catch { + return 18789; + } + })(); + return NextResponse.json({ + ok: true, + data: null, + status: "healthy", + checkedAt, + responseMs, + webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`, + }); + } return NextResponse.json({ ok: false, - error: msg, + error: cli.error || raw, status: "down", checkedAt, responseMs,