"use client"; import { ArrowClockwiseIcon, CheckCircleIcon, DatabaseIcon, FloppyDiskIcon, KeyIcon, PlugIcon, WarningCircleIcon, XIcon, } from "@phosphor-icons/react"; import { useEffect, useMemo, useState } from "react"; import type { SettingField, SettingsReadiness, SettingsResponse, TestResult, } from "@/types/settings"; const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000"; const testLabels: Record = { infrastructure: "测试数据库与队列", storage: "测试 MinIO", baidu_ocr: "测试百度 OCR", ai_models: "验证 API 与模型", gpu: "测试 GPU Worker", langfuse: "测试 Langfuse", sentry: "测试 Sentry", }; type SettingsCenterProps = { open: boolean; onClose: () => void; onReadinessChange: (readiness: SettingsReadiness) => void; }; function isVisible(field: SettingField, draft: Record) { if (!field.visible_when) return true; return Object.entries(field.visible_when).every(([key, expected]) => { if (key.endsWith("__not")) return draft[key.slice(0, -5)] !== expected; return draft[key] === expected; }); } function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : "请求失败,请检查 API 服务是否启动。"; } export function SettingsCenter({ open, onClose, onReadinessChange }: SettingsCenterProps) { const [data, setData] = useState(null); const [draft, setDraft] = useState>({}); const [activeCategoryId, setActiveCategoryId] = useState("deployment"); const [showAdvanced, setShowAdvanced] = useState(false); const [busy, setBusy] = useState(null); const [notice, setNotice] = useState<{ tone: "success" | "error"; text: string } | null>(null); const [testResults, setTestResults] = useState>({}); const activeCategory = useMemo( () => data?.categories.find((category) => category.id === activeCategoryId) ?? data?.categories[0], [activeCategoryId, data], ); function applyResponse(response: SettingsResponse) { setData(response); setDraft(response.values); onReadinessChange(response.readiness); } async function loadSettings() { setBusy("load"); setNotice(null); try { const response = await fetch(`${API_BASE}/v1/settings`, { cache: "no-store" }); if (!response.ok) throw new Error("系统设置读取失败。请确认后端 API 已启动。"); applyResponse(await response.json() as SettingsResponse); } catch (error) { setNotice({ tone: "error", text: getErrorMessage(error) }); } finally { setBusy(null); } } useEffect(() => { if (!open) return; let cancelled = false; fetch(`${API_BASE}/v1/settings`, { cache: "no-store" }) .then((response) => { if (!response.ok) throw new Error("系统设置读取失败。请确认后端 API 已启动。"); return response.json() as Promise; }) .then((response) => { if (cancelled) return; setData(response); setDraft(response.values); onReadinessChange(response.readiness); }) .catch((error: unknown) => { if (!cancelled) setNotice({ tone: "error", text: getErrorMessage(error) }); }); return () => { cancelled = true; }; }, [open, onReadinessChange]); function updateField(key: string, value: unknown) { setDraft((current) => ({ ...current, [key]: value })); setNotice(null); } async function saveSettings(showSuccess = true) { setBusy("save"); setNotice(null); try { const response = await fetch(`${API_BASE}/v1/settings`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: draft }), }); if (!response.ok) throw new Error("保存失败,请检查填写内容。"); const payload = await response.json() as SettingsResponse; applyResponse(payload); if (showSuccess) setNotice({ tone: "success", text: "设置已加密保存。" }); return payload; } catch (error) { setNotice({ tone: "error", text: getErrorMessage(error) }); throw error; } finally { setBusy(null); } } async function testTarget(target: string) { setBusy(target); setNotice(null); try { await saveSettings(false); setBusy(target); const response = await fetch(`${API_BASE}/v1/settings/test`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ target, values: {} }), }); const result = await response.json() as TestResult; setTestResults((current) => ({ ...current, [target]: result })); await loadSettings(); setNotice({ tone: result.ok ? "success" : "error", text: result.message }); } catch (error) { setNotice({ tone: "error", text: getErrorMessage(error) }); } finally { setBusy(null); } } async function generateFor(field: SettingField) { if (!field.generator) return; setBusy(field.key); try { const response = await fetch(`${API_BASE}/v1/settings/generate-secret`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: field.generator }), }); if (!response.ok) throw new Error("密钥生成失败。"); const payload = await response.json() as { value: string }; updateField(field.key, payload.value); setNotice({ tone: "success", text: `${field.label}已生成,点击保存后会加密存储。` }); } catch (error) { setNotice({ tone: "error", text: getErrorMessage(error) }); } finally { setBusy(null); } } function closeSafely() { if (data) { const secretKeys = new Set(data.categories.flatMap((category) => category.fields) .filter((field) => field.secret) .map((field) => field.key)); setDraft((current) => Object.fromEntries( Object.entries(current).filter(([key]) => !secretKeys.has(key)), )); } onClose(); } if (!open) return null; return (

首次使用向导

系统设置

只填写你真正使用的服务;密钥保存后不会再次显示。

{data ? (
{data.readiness.ready ? : }
{data.readiness.ready ? "工作流已就绪" : "完成必备配置后才能开始设计"}

{data.readiness.completed_required} / {data.readiness.total_required} 个必备分类已配置并测试

) : null}
{busy === "load" && !data ? (
正在读取设置
) : activeCategory && data ? ( <>

{activeCategory.label}

{activeCategory.description}

{activeCategory.fields.some((field) => field.advanced) ? ( ) : null}
{activeCategory.fields.map((field) => { if (!isVisible(field, draft) || (field.advanced && !showAdvanced)) return null; return ( updateField(field.key, value)} onGenerate={() => void generateFor(field)} busy={busy === field.key} /> ); })}
{activeCategory.id === "deployment" ? (

数据库和 Redis 密码存在“先有服务还是先打开设置页”的启动顺序问题,因此不在这里修改。运行 scripts/bootstrap.ps1 时会一次性安全生成,你不需要安装 OpenSSL。

) : null}
{activeCategory.test_targets.map((target) => { const recorded = testResults[target] ?? (data.tests[target] as TestResult | undefined); return ( ); })}
) : null}
{notice?.tone === "error" ? : null} {notice?.tone === "success" ? : null} {notice?.text ?? "必备项完成并测试通过后,设计工作流会自动解锁。"}
); } type SettingControlProps = { field: SettingField; value: unknown; configured: boolean; onChange: (value: unknown) => void; onGenerate: () => void; busy: boolean; }; function SettingControl({ field, value, configured, onChange, onGenerate, busy }: SettingControlProps) { const selectedOption = field.options.find((option) => option.value === String(value ?? "")); if (field.kind === "status") { return (
{configured ? : }
{field.label}

{field.description}

{configured ? "已配置" : "缺失"}
); } if (field.kind === "toggle") { return ( ); } return ( ); }