import { Eye, EyeOff, KeyRound, RefreshCw, ServerCog } from "lucide-react"; import { useCallback, useEffect, useState, type FormEvent } from "react"; import { toast } from "sonner"; import { adminApi, ApiError } from "../../api/client"; import type { ManagedProviderId, ManagedProviderOverview, ManagedProviderStatus, } from "../../api/types"; import { Badge, Button, Card, Dialog, ErrorState, Input, LoadingState, PageHeader, } from "../../components/primitives"; import { formatDateTime } from "../../lib/format"; const PROVIDERS: Array<{ id: ManagedProviderId; name: string; description: string; }> = [ { id: "deepseek", name: "DeepSeek", description: "用于润色、AI 和 Agent 等托管文本能力。", }, { id: "volcengine", name: "火山引擎", description: "用于托管语音识别能力,仅替换 API Key。", }, ]; export function ProvidersPage() { const [overview, setOverview] = useState(); const [loading, setLoading] = useState(true); const [error, setError] = useState(); const load = useCallback(async () => { setLoading(true); setError(undefined); try { setOverview(await adminApi.providers()); } catch (cause) { setError(cause); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const updateStatus = useCallback((updated: ManagedProviderStatus) => { setOverview((current) => { if (!current) return current; const providers = current.filter( (item) => item.providerId !== updated.providerId, ); return [...providers, updated]; }); }, []); if (loading) return ; if (error || !overview) return void load()} />; return (
void load()}> 刷新状态 } />

查看当前 Key 需要再次验证动态验证码;查看与替换都会写入审计日志,但不会记录密钥内容。

{PROVIDERS.map((provider) => ( item.providerId === provider.id)} onUpdated={updateStatus} /> ))}
); } function ProviderCard({ providerId, name, description, status, onUpdated, }: { providerId: ManagedProviderId; name: string; description: string; status?: ManagedProviderStatus; onUpdated: (status: ManagedProviderStatus) => void; }) { const [apiKey, setApiKey] = useState(""); const [currentApiKey, setCurrentApiKey] = useState(); const [revealOpen, setRevealOpen] = useState(false); const [totpCode, setTotpCode] = useState(""); const [revealError, setRevealError] = useState(""); const [revealing, setRevealing] = useState(false); const [saving, setSaving] = useState(false); function toggleCurrentApiKey() { if (currentApiKey) { setCurrentApiKey(undefined); return; } setTotpCode(""); setRevealError(""); setRevealOpen(true); } function handleRevealOpenChange(open: boolean) { setRevealOpen(open); if (!open) { setTotpCode(""); setRevealError(""); } } async function reveal(event: FormEvent) { event.preventDefault(); if (!/^\d{6}$/.test(totpCode)) { setRevealError("请输入 6 位动态验证码"); return; } setRevealing(true); setRevealError(""); try { const response = await adminApi.revealProviderApiKey(providerId, { totpCode }); setCurrentApiKey(response.apiKey); handleRevealOpenChange(false); } catch (error) { setRevealError(error instanceof ApiError ? error.message : "当前 API Key 读取失败"); } finally { setRevealing(false); } } async function submit(event: FormEvent) { event.preventDefault(); const normalized = apiKey.trim(); if (!validApiKey(normalized)) { toast.error("API Key 必须为 1–512 个字符,且不能包含换行"); return; } if (!window.confirm(`确定替换 ${name} API Key?保存后,新请求将立即使用该 Key。`)) { return; } setSaving(true); try { const updated = await adminApi.updateProviderApiKey(providerId, { apiKey: normalized, }); setApiKey(""); setCurrentApiKey((current) => (current ? normalized : undefined)); onUpdated(updated); toast.success(`${name} API Key 已替换`); } catch (error) { toast.error(error instanceof ApiError ? error.message : `${name} API Key 替换失败`); } finally { setSaving(false); } } return (

{name}

{status?.configured ? "已配置" : "未配置"}

{description}

点击眼睛并通过动态验证码验证后显示;再次点击或离开页面时隐藏。

void submit(event)}>

留空不会修改配置。若当前 Key 已显示,替换成功后会同步显示新 Key。

void reveal(event)} noValidate> {revealError ? (

{revealError}

) : null}
); } function StatusItem({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function sourceLabel(status?: ManagedProviderStatus): string { if (!status?.configured) return "尚未配置"; return status.source === "RUNTIME_OVERRIDE" ? "管理后台" : "环境变量"; } function validApiKey(value: string): boolean { return ( value.length >= 1 && value.length <= 512 && !value.includes("\n") && !value.includes("\r") ); }