# GHCS v0.2 · keys/page.tsx · API密钥管理前端 部署路径:`~/guanghu-dev/ghcs-mono/frontend/src/app/console/keys/page.tsx` ```tsx "use client"; import { useState, useEffect, useCallback } from "react"; import { KeyIcon, PlusIcon, TrashIcon, EyeIcon, EyeSlashIcon, PencilIcon, ShieldCheckIcon, XMarkIcon, CheckIcon, ArrowPathIcon, } from "@heroicons/react/24/outline"; const SUPPLIER_COLORS: Record = { deepseek: "#4F46E5", qwen: "#0EA5E9", glm: "#8B5CF6", moonshot: "#10B981", other: "#6B7280", }; type Supplier = { key: string; name: string; color: string; key_count: number; active_count: number; }; type ApiKey = { id: string; supplier: string; supplier_name: string; key_name: string; api_key_masked: string; is_active: boolean; monthly_budget: number | null; created_at: string; updated_at: string; }; type UsageItem = { id: string; supplier: string; key_name: string; total_calls: number; total_tokens: number; estimated_cost: number; monthly_budget: number | null; budget_used_percent: number | null; is_active: boolean; }; type Alert = { level: "red" | "yellow"; key_id: string; key_name: string; supplier: string; message: string; }; export default function ApiKeysPage() { const [suppliers, setSuppliers] = useState([]); const [keys, setKeys] = useState([]); const [usage, setUsage] = useState([]); const [alerts, setAlerts] = useState([]); const [loading, setLoading] = useState(true); const [showModal, setShowModal] = useState(false); const [editingKey, setEditingKey] = useState(null); const [form, setForm] = useState({ supplier: "deepseek", key_name: "", api_key: "", monthly_budget: "", }); const [showKey, setShowKey] = useState(false); const [activeTab, setActiveTab] = useState<"keys" | "usage">("keys"); const API_BASE = process.env.NEXT_PUBLIC_CONSOLE_API || ""; const fetchAll = useCallback(async () => { try { const [supRes, keyRes, usageRes, healthRes] = await Promise.all([ fetch(`${API_BASE}/api/keys/suppliers`), fetch(`${API_BASE}/api/keys/list`), fetch(`${API_BASE}/api/keys/usage`), fetch(`${API_BASE}/api/keys/health-check`), ]); if (supRes.ok) setSuppliers((await supRes.json()).suppliers); if (keyRes.ok) setKeys((await keyRes.json()).keys); if (usageRes.ok) setUsage((await usageRes.json()).items); if (healthRes.ok) setAlerts((await healthRes.json()).alerts); } catch (e) { console.error("加载失败", e); } finally { setLoading(false); } }, [API_BASE]); useEffect(() => { fetchAll(); }, [fetchAll]); const openAdd = (supplier: string) => { setEditingKey(null); setForm({ supplier, key_name: "", api_key: "", monthly_budget: "" }); setShowKey(false); setShowModal(true); }; const openEdit = (key: ApiKey) => { setEditingKey(key); setForm({ supplier: key.supplier, key_name: key.key_name, api_key: "", monthly_budget: key.monthly_budget?.toString() || "", }); setShowKey(false); setShowModal(true); }; const handleSubmit = async () => { if (!form.key_name) return alert("请输入密钥名称"); const url = editingKey ? `${API_BASE}/api/keys/${editingKey.id}` : `${API_BASE}/api/keys/add`; const method = editingKey ? "PUT" : "POST"; const body: Record = { supplier: form.supplier, key_name: form.key_name, }; if (form.api_key) body.api_key = form.api_key; if (form.monthly_budget) body.monthly_budget = parseFloat(form.monthly_budget); const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (res.ok) { setShowModal(false); fetchAll(); } else { const err = await res.json(); alert("操作失败: " + (err.detail || "未知错误")); } }; const handleDelete = async (keyId: string, keyName: string) => { if (!confirm(`确认删除密钥 "${keyName}"?此操作不可恢复。`)) return; const res = await fetch(`${API_BASE}/api/keys/${keyId}`, { method: "DELETE" }); if (res.ok) fetchAll(); }; const handleToggleActive = async (key: ApiKey) => { const res = await fetch(`${API_BASE}/api/keys/${key.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ is_active: !key.is_active }), }); if (res.ok) fetchAll(); }; const totalCost = usage.reduce((s, u) => s + u.estimated_cost, 0); const totalCalls = usage.reduce((s, u) => s + u.total_calls, 0); return (

API 密钥管理

管理商业API供应商密钥 · 用量监控 · 费用告警

{alerts.length > 0 && (
{alerts.map((a, i) => (
{a.key_name}{" "} ({a.supplier})

{a.message}

))}
)}
{activeTab === "keys" ? (
{loading ? (
加载中...
) : ( suppliers.map((sup) => { const supKeys = keys.filter((k) => k.supplier === sup.key); return (

{sup.name}

{sup.active_count}/{sup.key_count} 活跃
{supKeys.length === 0 ? (
暂无密钥,点击上方按钮添加
) : (
{supKeys.map((key) => (
{key.key_name} {key.api_key_masked}
创建: {key.created_at.slice(0, 10)} {key.updated_at !== key.created_at && ` · 更新: ${key.updated_at.slice(0, 10)}`}
))}
)}
); }) )}
) : (
总调用次数
{totalCalls.toLocaleString()}
预估月费
¥{totalCost.toFixed(2)}
活跃密钥
{usage.filter((u) => u.is_active).length}
{usage.length === 0 ? (
暂无调用记录
) : (
{usage.map((u) => ( ))}
密钥 供应商 调用次数 Token 用量 预估费用 预算
{u.key_name} {u.supplier} {u.total_calls.toLocaleString()} {u.total_tokens.toLocaleString()} ¥{u.estimated_cost.toFixed(2)} {u.monthly_budget ? (
= 100 ? "bg-red-500" : (u.budget_used_percent || 0) >= 80 ? "bg-yellow-500" : "bg-green-500" }`} style={{ width: `${Math.min(u.budget_used_percent || 0, 100)}%`, }} />
{u.budget_used_percent}%
) : ( )}
)}
)} {showModal && (

{editingKey ? "编辑密钥" : "添加密钥"}

setForm({ ...form, key_name: e.target.value })} className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-gray-100 text-sm focus:outline-none focus:border-indigo-500" />
setForm({ ...form, api_key: e.target.value })} className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 pr-10 text-gray-100 text-sm font-mono focus:outline-none focus:border-indigo-500" />
setForm({ ...form, monthly_budget: e.target.value })} className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-gray-100 text-sm focus:outline-none focus:border-indigo-500" />
)}
); } ``` --- ## 📁 部署 下载这个文件,然后拖到 VS Code 里替换: [keys-page.tsx](GHCS%20v0%202%20%C2%B7%20keys%20page%20tsx%20%C2%B7%20API%E5%AF%86%E9%92%A5%E7%AE%A1%E7%90%86%E5%89%8D%E7%AB%AF/keys-page.tsx) keys-page.tsx 或者终端里执行: ```bash base64 -d << 'B64END' > ~/guanghu-dev/ghcs-mono/frontend/src/app/console/keys/page.tsx base64_data B64END ``` 保存后刷新 [http://localhost:3000/console/keys](http://localhost:3000/console/keys)