feat: 独立运维控制台 (sundynix-admin) — LLM 模型配置控制面前端

新增独立 Vite+React 控制台(:5174),运维在此配置控制面,区别于桌面端构建者 UI。

- sundynix-admin: 模型页(列表[激活徽标/脱敏key] + 登记表单 + 测试连接 + 激活/删除)
  → 调 Gateway /api/v1/admin/models*;数据源/租户/护栏 占位(规划中);Gateway 健康灯
- gateway CORS 补 DELETE(控制台删除模型用)
- Makefile admin 目标(Vite :5174); .claude/launch.json 加 admin-console

验证: npm build✓; 真实浏览器跑通——控制台拉到真实模型列表, 填表→测试连接
✓连接成功(HTTP 200,browser→GW→mock), 保存→表格新增行。激活会经 NATS 热更新
Dispatcher(上一提交已证)。控制台↔控制面↔Dispatcher 全链路打通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-10 15:54:01 +08:00
parent 3c65189f30
commit 6f5b98f186
16 changed files with 3085 additions and 3 deletions
+80
View File
@@ -0,0 +1,80 @@
import { useEffect, useState } from "react";
import { ModelsPage } from "./ModelsPage";
import { gatewayOnline } from "./api";
type Tab = "models" | "datasources" | "tenants" | "guardrails";
const NAV: Array<{ key: Tab; label: string; ready?: boolean }> = [
{ key: "models", label: "模型", ready: true },
{ key: "datasources", label: "数据源" },
{ key: "tenants", label: "租户" },
{ key: "guardrails", label: "护栏" },
];
function Soon({ title, desc }: { title: string; desc: string }) {
return (
<div className="rounded-lg border border-dashed bg-gray-50 p-6">
<div className="mb-1 text-sm font-semibold text-gray-600">{title}</div>
<p className="text-xs leading-relaxed text-gray-400">{desc}</p>
<span className="mt-3 inline-block rounded bg-gray-200 px-2 py-0.5 text-[10px] text-gray-500"></span>
</div>
);
}
export default function App() {
const [tab, setTab] = useState<Tab>("models");
const [online, setOnline] = useState(false);
useEffect(() => {
const ping = () => gatewayOnline().then(setOnline);
ping();
const id = setInterval(ping, 4000);
return () => clearInterval(id);
}, []);
return (
<div className="flex h-screen w-screen text-gray-900">
<aside className="flex w-56 shrink-0 flex-col border-r bg-gray-50">
<div className="border-b p-4">
<div className="text-sm font-bold text-gray-800">sundynix-agentix</div>
<div className="text-[11px] text-gray-400"></div>
</div>
<nav className="flex flex-col p-2">
{NAV.map((n) => (
<button
key={n.key}
onClick={() => setTab(n.key)}
className={`flex items-center justify-between rounded px-3 py-2 text-left text-sm ${
tab === n.key ? "bg-violet-50 font-medium text-violet-700" : "text-gray-600 hover:bg-gray-100"
}`}
>
{n.label}
{!n.ready && <span className="text-[9px] text-gray-300"></span>}
</button>
))}
</nav>
<div className="mt-auto flex items-center gap-2 border-t p-4 text-[11px] text-gray-500">
<span className={`h-2 w-2 rounded-full ${online ? "bg-emerald-500" : "bg-rose-500"}`} />
Gateway {online ? "在线" : "离线"}
</div>
</aside>
<main className="flex-1 overflow-auto p-6">
<h1 className="mb-4 text-lg font-semibold text-gray-800">
{NAV.find((n) => n.key === tab)?.label}
</h1>
{tab === "models" && <ModelsPage />}
{tab === "datasources" && (
<Soon
title="数据源(向量库 / 图库 / 全文)"
desc="配置 Milvus(:19530) / Neo4j(:7687) / Bleve 连接 + 测试连接 + 状态。后端 search.Hybrid 接真后接通(RAG 核心链)。"
/>
)}
{tab === "tenants" && (
<Soon title="租户 / 工作区" desc="多租户隔离、配额、用户与计费。垂直行业平台级复制的基座。" />
)}
{tab === "guardrails" && (
<Soon title="护栏" desc="输入/输出 Guardrail 规则(脱敏 / 免责 / 强制引用)。受监管垂直必备。" />
)}
</main>
</div>
);
}