# GHCS v0.2 · keys/page.tsx 完整可替换版(含登录入口+鉴权) 本页提供一份 **可直接全量替换** 的 `page.tsx` 代码:包含 - ✅ 登录入口(POST /api/login → localStorage.ghcs_access_token) - ✅ 写操作自动带 Bearer(新增/编辑/启停/删除都不再 401) - ✅ 去掉 `any`(满足 `@typescript-eslint/no-explicit-any`) - ✅ token 初始化不使用 `setState in effect`(用 lazy initializer) > 替换路径:`~/guanghu-dev/ghcs-mono/frontend/src/app/console/keys/page.tsx` > --- ## ✅ 完整代码(直接全选复制) ```tsx "use client"; import { useState, useEffect, useCallback } from "react"; import { KeyIcon, PlusIcon, TrashIcon, 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; }; type ApiKeyForm = { supplier: string; key_name: string; api_key: string; monthly_budget: string; }; export default function ApiKeysPage() { const API_BASE = (process.env.NEXT_PUBLIC_CONSOLE_API as string) || ""; // ===== Auth ===== const [token, setToken] = useState(() => { if (typeof window === "undefined") return ""; return localStorage.getItem("ghcs_access_token") || ""; }); const [loginForm, setLoginForm] = useState({ username: "awen", password: "" }); const [authError, setAuthError] = useState(""); const authHeaders = () => { return token ? { Authorization: `Bearer ${token}` } : {}; }; const handleLogin = async () => { setAuthError(""); try { const res = await fetch(`${API_BASE}/api/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: loginForm.username, password: loginForm.password, }), }); if (!res.ok) { const errText = await res.text(); setAuthError("登录失败: " + errText); return; } const data = (await res.json()) as { access_token?: string }; const accessToken = data.access_token; if (!accessToken) { setAuthError("登录失败:未返回 access_token"); return; } localStorage.setItem("ghcs_access_token", accessToken); setToken(accessToken); setLoginForm((f) => ({ ...f, password: "" })); } catch (e: unknown) { setAuthError("登录异常: " + String(e)); } }; const handleLogout = () => { localStorage.removeItem("ghcs_access_token"); setToken(""); setAuthError(""); }; // ===== Data state ===== 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 [activeTab, setActiveTab] = useState<"keys" | "usage">("keys"); 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 as Supplier[]); if (keyRes.ok) setKeys((await keyRes.json()).keys as ApiKey[]); if (usageRes.ok) setUsage((await usageRes.json()).items as UsageItem[]); if (healthRes.ok) setAlerts((await healthRes.json()).alerts as Alert[]); } catch { // ignore } finally { setLoading(false); } }, [API_BASE]); // NOTE: 你们仓库当前 eslint 规则会对 effect 内触发 setState 很敏感。 // 这里允许一次性初始化拉取。若你未来要“lint 全绿”,可以统一调整 eslint 规则或改为路由加载器。 // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { fetchAll(); }, [fetchAll]); const openAdd = (supplier: string) => { setEditingKey(null); setForm({ supplier, key_name: "", api_key: "", monthly_budget: "" }); 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() || "", }); 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", ...authHeaders(), }, body: JSON.stringify(body), }); if (!res.ok) { const errText = await res.text(); alert("操作失败: " + errText); return; } setShowModal(false); await fetchAll(); }; const handleDelete = async (keyId: string, keyName: string) => { if (!confirm(`确认删除密钥 "${keyName}"?此操作不可恢复。`)) return; const res = await fetch(`${API_BASE}/api/keys/${keyId}`, { method: "DELETE", headers: { ...authHeaders(), }, }); if (!res.ok) { const errText = await res.text(); alert("删除失败: " + errText); return; } await fetchAll(); }; const handleToggleActive = async (key: ApiKey) => { const res = await fetch(`${API_BASE}/api/keys/${key.id}`, { method: "PUT", headers: { "Content-Type": "application/json", ...authHeaders(), }, body: JSON.stringify({ is_active: !key.is_active }), }); if (!res.ok) { const errText = await res.text(); alert("启停失败: " + errText); return; } await 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供应商密钥 · 用量监控 · 费用告警

{/* Login bar */}
{token ? (
已登录(Bearer token 已加载) {token.slice(0, 12)}…{token.slice(-8)}
) : (
未登录:写操作(新增/启停/删除)会 401。请先登录获取 token。
setLoginForm((f) => ({ ...f, username: e.target.value }))} placeholder="username" className="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-gray-100 text-sm" /> setLoginForm((f) => ({ ...f, password: e.target.value }))} placeholder="password" className="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-gray-100 text-sm" />
{authError &&
{authError}
}
)}
{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 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" />
)}
); } ``` --- ## 使用说明(复制替换步骤) 1. VS Code 打开文件:`frontend/src/app/console/keys/page.tsx` 2. `Cmd+A` 全选 → 粘贴本页代码 → `Cmd+S` 保存 3. 前端重启:`Ctrl+C` 停掉 `npm run dev` → 再 `npm run dev` 4. 打开:`http://localhost:3000/console/keys` 5. 登录:`awen / guanghu2026` --- ## 验收口径 - Network:`POST /api/login` → 200 - localStorage:`ghcs_access_token` 有值 - 启停:`PUT /api/keys/{id}` → 200(不再 401) - 新增:`POST /api/keys/add` → 200 - 删除:`DELETE /api/keys/{id}` → 200