"use client"; import { useEffect, useState } from "react"; import { fetchCsrf, getTtsSettings, testTtsSettings, updateTtsSettings, getUsageSummary, listAuditLogs, listTtsTasks, type TtsSettings, type TtsSettingsTestResult } from "@/lib/admin"; import { BrandIcon } from "@/components/brand-icon"; import { StatusCard } from "@/components/ui"; type Task = { id: string; user_id: string; user_email: string; status: string; text_length: number; provider_voice_id: string; error_code: string | null; attempt_count: number; reserved_amount: number; created_at: string; started_at: string | null; finished_at: string | null; duration_ms: number | null; settlement: string; reserved: number; used: number; }; type Audit = { id: string; actor_email: string | null; action: string; target_type: string; target_id: string; before_value: Record | null; after_value: Record | null; reason: string; created_at: string; }; type Summary = { tasks: { total: number; by_status: Record; failed: number }; quota: { used: number; reserved: number; adjustment: number }; ledger_total: number }; const statusLabel: Record = { queued: "排队", running: "进行中", succeeded: "成功", failed: "失败" }; const settleLabel: Record = { consumed: "已消费", released: "已释放", pending: "待结算" }; const actionLabel: Record = { user_status: "用户状态变更", membership: "授予/变更会员", membership_revoke: "撤销会员", quota_adjustment: "额度调整", tts_settings_update: "TTS 配置变更", }; type ConfigForm = { upstream_url: string; api_key: string; timeout_seconds: number; default_model: string; reason: string }; function TtsConfigPanel() { const [csrf, setCsrf] = useState(""); const [current, setCurrent] = useState(null); const [form, setForm] = useState({ upstream_url: "", api_key: "", timeout_seconds: 120, default_model: "qwen3-tts", reason: "" }); const [busy, setBusy] = useState(false); const [testing, setTesting] = useState(false); const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null); const [testResult, setTestResult] = useState(null); async function load() { try { const s = await getTtsSettings(); setCurrent(s); setForm((f) => ({ ...f, upstream_url: s.upstream_url, timeout_seconds: s.timeout_seconds, default_model: s.default_model })); } catch (e) { setMessage({ ok: false, text: e instanceof Error ? e.message : "加载配置失败" }); } } useEffect(() => { void fetchCsrf().then(setCsrf).catch(() => undefined); void load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function save() { setBusy(true); setMessage(null); try { await updateTtsSettings({ upstream_url: form.upstream_url.trim(), api_key: form.api_key.trim() || undefined, timeout_seconds: form.timeout_seconds, default_model: form.default_model.trim(), reason: form.reason.trim() }, csrf); setForm((f) => ({ ...f, api_key: "" })); setTestResult(null); setMessage({ ok: true, text: "配置已保存,新任务立即生效" }); await load(); } catch (e) { setMessage({ ok: false, text: e instanceof Error ? e.message : "保存失败" }); } finally { setBusy(false); } } async function probe() { setTesting(true); setTestResult(null); try { const res = await testTtsSettings({ upstream_url: form.upstream_url.trim() || undefined, api_key: form.api_key.trim() || undefined, timeout_seconds: form.timeout_seconds, default_model: form.default_model.trim() || undefined }, csrf); setTestResult(res); } catch (e) { setTestResult({ ok: false, message: e instanceof Error ? e.message : "探测请求失败" }); } finally { setTesting(false); } } return (

TTS 上游配置

留空 API Key 表示保持不变。保存后新的生成任务立即使用新配置;建议先探测、确认成功再保存。探测会消耗上游一次生成额度。

setForm((f) => ({ ...f, upstream_url: e.target.value }))} />
setForm((f) => ({ ...f, api_key: e.target.value }))} />
setForm((f) => ({ ...f, timeout_seconds: Math.max(5, Math.min(600, Number(e.target.value) || 120)) }))} />
setForm((f) => ({ ...f, default_model: e.target.value }))} />
setForm((f) => ({ ...f, reason: e.target.value }))} />
{message ?

{message.text}

: null}

当前生效配置

{current ? (
来源
{current.source === "db" ? "管理后台(数据库)" : "服务器环境变量"}
上游地址
{current.upstream_url || "未配置"}
API Key
{current.api_key_set ? current.api_key_masked : "未配置"}
超时 / 默认模型
{current.timeout_seconds}s · {current.default_model}
{current.updated_at ?
最近修改
{new Date(current.updated_at).toLocaleString()}
: null}
) :

加载中…

}
{testResult ? (

探测结果

{testResult.message}

{[testResult.status_code != null ? `HTTP ${testResult.status_code}` : null, testResult.latency_ms != null ? `耗时 ${testResult.latency_ms}ms` : null, testResult.content_type ? testResult.content_type : null, testResult.size_bytes ? `${(testResult.size_bytes / 1024).toFixed(1)} KB` : null].filter(Boolean).join(" · ")}

) : null}
); } export function TtsAuditPanel() { const [tab, setTab] = useState<"tasks" | "config" | "audit">("tasks"); const [status, setStatus] = useState(""); const [taskPage, setTaskPage] = useState(1); const [tasks, setTasks] = useState([]); const [taskTotal, setTaskTotal] = useState(0); const [taskPages, setTaskPages] = useState(1); const [auditPage, setAuditPage] = useState(1); const [audits, setAudits] = useState([]); const [auditTotal, setAuditTotal] = useState(0); const [auditPages, setAuditPages] = useState(1); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); async function loadTasks(p = taskPage) { setLoading(true); setError(""); try { const res = await listTtsTasks({ status, page: p, limit: 20 }); setTasks(res.items as unknown as Task[]); setTaskTotal(res.total); setTaskPages(res.pages); setTaskPage(res.page); } catch (e) { setError(e instanceof Error ? e.message : "加载失败"); } finally { setLoading(false); } } async function loadAudit(p = auditPage) { setLoading(true); setError(""); try { const res = await listAuditLogs({ page: p, limit: 30 }); setAudits(res.items as unknown as Audit[]); setAuditTotal(res.total); setAuditPages(res.pages); setAuditPage(res.page); } catch (e) { setError(e instanceof Error ? e.message : "加载失败"); } finally { setLoading(false); } } useEffect(() => { void loadTasks(1); void getUsageSummary().then(setSummary).catch(() => undefined); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function switchTab(next: "tasks" | "config" | "audit") { setTab(next); if (next === "tasks") void loadTasks(1); else if (next === "audit") void loadAudit(1); } return (
{summary ? (
) : null}
{error ?
{error}
: null} {tab === "config" ? : tab === "tasks" ? (
{loading ? ( ) : tasks.length === 0 ? ( ) : ( tasks.map((task) => ( )) )}
用户 状态 计量 耗时 结算 错误
加载中…
无匹配任务

{task.user_email}

{new Date(task.created_at).toLocaleString()}

{statusLabel[task.status] ?? task.status} {task.text_length} 字符 {task.duration_ms != null ? `${(task.duration_ms / 1000).toFixed(1)}s` : "—"} {settleLabel[task.settlement] ?? task.settlement} {task.error_code ?? "—"} {task.id.slice(0, 8)}
第 {taskPage} / {taskPages} 页
) : (

审计记录默认不含完整用户输入文本,也不显示任何密钥或令牌。账号禁用、会员变更、VIP 撤销和额度调整均记录操作者、前后值与原因。

{audits.length === 0 ? ( ) : ( audits.map((a) => ( )) )}
操作者 动作 目标 原因 时间
暂无审计记录
{a.actor_email ?? "—"} {actionLabel[a.action] ?? a.action} {a.target_type} · {String(a.target_id).slice(0, 8)} {a.reason} {new Date(a.created_at).toLocaleString()}
第 {auditPage} / {auditPages} 页
)}
); }