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 影响。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注