"use client"; import { useEffect, useState } from "react"; import { KeyRound, RefreshCcw, Save } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; type EditableConfig = { key: string; value: string; description: string; }; type SensitiveConfig = { key: string; label: string; description: string; configured: boolean; masked: string; length: number; updated_at: string; updated_by: string; source: string; }; type SensitiveHealth = { ok?: boolean; status?: number; latency_ms?: number; points?: number; observation_time_local?: string; error?: string; }; export function ConfigPageClient() { const [configs, setConfigs] = useState([]); const [sensitiveConfigs, setSensitiveConfigs] = useState([]); const [editing, setEditing] = useState>({}); const [sensitiveEditing, setSensitiveEditing] = useState>({}); const [saving, setSaving] = useState(false); const [sensitiveSaving, setSensitiveSaving] = useState(false); const [result, setResult] = useState(""); const [sensitiveResult, setSensitiveResult] = useState(""); const [sensitiveHealth, setSensitiveHealth] = useState(null); const [sensitiveCheckedAt, setSensitiveCheckedAt] = useState(""); const load = async () => { try { const [res, sensitiveRes] = await Promise.all([ fetch("/api/ops/config"), fetch("/api/ops/sensitive-config"), ]); if (res.ok) { const data = (await res.json()) as { configs?: EditableConfig[] }; setConfigs(data.configs ?? []); } if (sensitiveRes.ok) { const data = (await sensitiveRes.json()) as { configs?: SensitiveConfig[] }; setSensitiveConfigs(data.configs ?? []); } } catch { /* backend not ready yet */ } }; const handleSave = async (key: string) => { const newVal = editing[key]; if (newVal == null) return; setSaving(true); setResult(""); try { const res = await fetch("/api/ops/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: newVal }), }); if (res.ok) { setResult(`${key} 已更新`); setConfigs((prev) => prev.map((c) => (c.key === key ? { ...c, value: newVal } : c))); setEditing((prev) => { const n = { ...prev }; delete n[key]; return n; }); } else { setResult(`保存失败: ${await res.text().catch(() => "")}`); } } catch { setResult("保存失败"); } setSaving(false); }; const handleSensitiveSave = async (key: string) => { const newVal = sensitiveEditing[key]?.trim(); if (!newVal) return; setSensitiveSaving(true); setSensitiveResult(""); setSensitiveHealth(null); setSensitiveCheckedAt(""); try { const res = await fetch("/api/ops/sensitive-config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: newVal }), }); if (res.ok) { const data = (await res.json()) as { config?: SensitiveConfig; health?: SensitiveHealth | null; }; if (data.config) { setSensitiveConfigs((prev) => prev.map((cfg) => (cfg.key === key ? data.config as SensitiveConfig : cfg))); } setSensitiveEditing((prev) => { const n = { ...prev }; delete n[key]; return n; }); setSensitiveHealth(data.health ?? null); setSensitiveCheckedAt(new Date().toLocaleString("zh-CN", { hour12: false })); setSensitiveResult(`${key} 已轮换`); } else { setSensitiveResult(`轮换失败: ${await res.text().catch(() => "")}`); } } catch { setSensitiveResult("轮换失败"); } setSensitiveSaving(false); }; useEffect(() => { void load(); }, []); return (

系统配置

可编辑配置 {configs.length === 0 ? (

配置 API 尚未就绪(需要后端支持)

) : (
{configs.map((cfg) => (
{cfg.key}
{cfg.description}
setEditing((prev) => ({ ...prev, [cfg.key]: e.target.value }))} className="w-24 rounded-lg border border-white/10 bg-black/30 px-3 py-1.5 text-sm text-white font-mono text-center outline-none focus:border-cyan-400/50" />
))}
)} {result && (

{result}

)}

仅显示非敏感配置项。修改后立即影响当前后端进程;需要跨重启持久化的密钥请使用下方凭证轮换模块。

敏感凭证轮换 {sensitiveConfigs.length === 0 ? (

敏感配置 API 尚未就绪。

) : (
{sensitiveConfigs.map((cfg) => (
{cfg.label}
{cfg.configured ? "已配置" : "未配置"} {cfg.source === "environment" ? "环境变量兜底" : "DB 持久化"}
{cfg.description}
当前值 {cfg.masked || "未设置"}
更新人 {cfg.updated_by || "-"}
更新时间 {cfg.updated_at || "-"}
setSensitiveEditing((prev) => ({ ...prev, [cfg.key]: e.target.value }))} className="min-w-0 flex-1 rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white font-mono outline-none focus:border-cyan-400/50" />

在 ops 页面粘贴真实的 $$ sessionId 即可;这里不是 Docker .env,不需要把 $$ 写成 $$$$。

))}
)} {sensitiveResult && (

{sensitiveResult}

)} {sensitiveHealth && (
AMSC 健康检查:{sensitiveHealth.ok ? "通过" : "失败"} {sensitiveCheckedAt ? ` · 最近检查 ${sensitiveCheckedAt}` : ""} {typeof sensitiveHealth.points === "number" ? ` · 跑道点 ${sensitiveHealth.points}` : ""} {sensitiveHealth.observation_time_local ? ` · 观测 ${sensitiveHealth.observation_time_local}` : ""} {sensitiveHealth.error ? ` · ${sensitiveHealth.error}` : ""}
)}

这里不会返回或展示明文。轮换值写入共享运行时数据库,后端和 Bot 会优先读取该值,环境变量仅作为兜底。

); }