VPS 自建 LLM 网关实战:one-api + OpenClaw + n8n + 监控面板优化全记录
第 4 页:集成到 VPS Monitor
架构
cron (04:00) → one-api-health.py → JSON 报告
↓
Next.js API: /api/one-api-health
↓
组件: OneApiHealth.tsx
↓
VPS Monitor Realtime tab(底部)
API 路由
// src/app/api/one-api-health/route.ts
import { NextResponse } from "next/server";
import fs from "fs";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
const REPORT = "/root/one-api-health-report.json";
export async function GET() {
try {
if (!fs.existsSync(/*turbopackIgnore: true*/ REPORT)) {
return NextResponse.json({ available: false });
}
const content = fs.readFileSync(/*turbopackIgnore: true*/ REPORT, "utf-8");
const data = JSON.parse(content);
return NextResponse.json({ available: true, ...data });
} catch (e: any) {
return NextResponse.json({ available: false, error: e.message });
}
}
React 组件(节选)
"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
export function OneApiHealth() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
async function load() {
try {
const r = await fetch("/api/one-api-health", { cache: "no-store" });
setData(await r.json());
} finally { setLoading(false); }
}
useEffect(() => {
load();
const t = setInterval(load, 30000);
return () => clearInterval(t);
}, []);
if (loading || !data?.available) return null;
return (
<Card>
<CardHeader className="pb-2">
<CardTitle>one-api 渠道健康</CardTitle>
<CardDescription>
{data.ok} OK / {data.fail} FAIL / {data.total} 合計
</CardDescription>
</CardHeader>
<CardContent>
{data.results.map((r: any) => (
<div key={r.id} className="flex items-center gap-2 text-[11px]">
<span className={r.status === "OK" ? "bg-green-500" : "bg-red-500"} />
<span>ch{r.id}</span>
<span>{r.model}</span>
<Badge>{r.status}</Badge>
</div>
))}
</CardContent>
</Card>
);
}
布局陷阱
最初把 <OneApiHealth /> 放在 realtime-tokens.tsx 内部底部(第 232 行),但渲染后总是在顶部。原因是组件受内部 CSS 影响。最终解决方案:把它从 RealtimeTokens 内部移出,放到 monitor-tabs.tsx 的 Realtime 容器里:
// monitor-tabs.tsx
{activeTab === "realtime" && (
<div className="p-6 space-y-4">
<RealtimeTokens />
<OneApiHealth />
</div>
)}
这样顺序由父组件硬编码控制,不再受子组件内部 CSS 影响。
