"use client"; import { ArrowClockwiseIcon, BrainIcon, CheckCircleIcon, DatabaseIcon, FloppyDiskIcon, ImageIcon, KeyIcon, PencilSimpleIcon, PlugIcon, PlusIcon, RobotIcon, TrashIcon, WarningCircleIcon, XIcon, } from "@phosphor-icons/react"; import { useEffect, useMemo, useState } from "react"; import type { ImageParameterProfile, ModelCapability, ModelCategory, ModelPoolItem, 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", gpu: "测试 GPU Worker", langfuse: "测试 Langfuse", sentry: "测试 Sentry", }; const capabilityLabels: Record = { orchestration: "总调度", spatial_understanding: "空间理解", image_generation: "图像生成", image_editing: "局部编辑", }; const providerLabels: Record = { lingke: "聚合引擎 AIGC", openai: "OpenAI 官方", custom: "其他兼容接口", }; const imagePresets: Record, Partial> = { gpt_image_2: { name: "GPT Image 2", model_id: "gpt-image-2", category: "multimodal", provider: "lingke", base_url: "https://api.lk888.ai", capabilities: ["image_generation", "image_editing"], models_path: "/v1/models", image_generation_path: "/v1/media/generate", image_edit_path: "/v1/media/generate", image_status_path: "/v1/media/status", image_protocol: "aigc_media", image_parameter_profile: "gpt_image_2", }, nano_banana_pro: { name: "Nano Banana Pro", model_id: "gemini-3-pro-image-preview", category: "multimodal", provider: "lingke", base_url: "https://api.lk888.ai", capabilities: ["image_generation", "image_editing"], models_path: "/v1/models", image_generation_path: "/v1/media/generate", image_edit_path: "/v1/media/generate", image_status_path: "/v1/media/status", image_protocol: "aigc_media", image_parameter_profile: "nano_banana_pro", }, seedream_5_pro: { name: "即梦 5.0 Pro", model_id: "doubao-seedream-5-0-pro-260628", category: "multimodal", provider: "lingke", base_url: "https://api.lk888.ai", capabilities: ["image_generation", "image_editing"], models_path: "/v1/models", image_generation_path: "/v1/media/generate", image_edit_path: "/v1/media/generate", image_status_path: "/v1/media/status", image_protocol: "aigc_media", image_parameter_profile: "seedream_5_pro", }, }; 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 服务是否启动。"; } async function responseError(response: Response, fallback: string) { try { const payload = await response.json() as { detail?: string | { message?: string } }; if (typeof payload.detail === "string") return payload.detail; if (payload.detail?.message) return payload.detail.message; } catch { // Use the readable fallback below when an upstream proxy returns non-JSON content. } return fallback; } function modelPoolFrom(draft: Record): ModelPoolItem[] { return Array.isArray(draft.model_pool) ? draft.model_pool as ModelPoolItem[] : []; } function modelCategoryReady(data: SettingsResponse) { return ![...data.readiness.missing, ...data.readiness.untested] .some((message) => message.startsWith("AI 模型:")); } 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"); 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); } 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(await responseError(response, "保存失败,请检查填写内容。")); 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 testModel(modelId: string) { const target = `model:${modelId}`; setBusy(target); setNotice(null); try { await saveSettings(false); setBusy(target); const response = await fetch(`${API_BASE}/v1/settings/models/${encodeURIComponent(modelId)}/test`, { method: "POST", }); 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 testModelGeneration(modelId: string) { if (!window.confirm("这会真实生成一张低成本测试图,并消耗该模型的一次调用额度。是否继续?")) return; const target = `generation:${modelId}`; setBusy(target); setNotice(null); try { await saveSettings(false); setBusy(target); const response = await fetch(`${API_BASE}/v1/settings/models/${encodeURIComponent(modelId)}/test-generation`, { method: "POST", }); 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 (

首次使用向导

系统设置

按服务分类配置并逐项测试。所有 API Key 都由后端加密保存。

{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.id !== "models" && activeCategory.fields.some((field) => field.advanced) ? ( ) : null}
{activeCategory.id === "models" ? ( void testModel(modelId)} onTrial={(modelId) => void testModelGeneration(modelId)} /> ) : (
{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 时会自动安全生成。

) : null} {activeCategory.id !== "models" ? (
{activeCategory.test_targets.map((target) => { const recorded = testResults[target] ?? (data.tests[target] as TestResult | undefined); return ( ); })}
) : null} ) : null}
{notice?.tone === "error" ? : null} {notice?.tone === "success" ? : null} {notice?.text ?? "必备项完成并测试通过后,设计工作流会自动解锁。"}
); } type ModelPoolPanelProps = { draft: Record; tests: Record; busy: string | null; onChange: (key: string, value: unknown) => void; onTest: (modelId: string) => void; onTrial: (modelId: string) => void; }; function newModel(category: ModelCategory): ModelPoolItem { return { id: crypto.randomUUID(), name: "", model_id: "", category, provider: "lingke", base_url: "", api_key: "", capabilities: category === "language" ? ["orchestration"] : ["spatial_understanding"], models_path: "/models", chat_path: "/chat/completions", image_generation_path: "/images/generations", image_edit_path: "/images/edits", image_status_path: "/v1/media/status", image_protocol: "openai_images", image_parameter_profile: "generic", enabled: true, api_key_configured: false, }; } function ModelPoolPanel({ draft, tests, busy, onChange, onTest, onTrial }: ModelPoolPanelProps) { const pool = modelPoolFrom(draft); const [editor, setEditor] = useState(null); const [editorError, setEditorError] = useState(""); const [showAdvanced, setShowAdvanced] = useState(false); const languageModels = pool.filter((item) => item.category === "language"); const multimodalModels = pool.filter((item) => item.category === "multimodal"); const spatialModels = multimodalModels.filter((item) => item.capabilities.includes("spatial_understanding")); const imageModels = multimodalModels.filter((item) => item.capabilities.includes("image_generation")); function replacePool(nextPool: ModelPoolItem[]) { onChange("model_pool", nextPool); } function applyPreset(profile: ImageParameterProfile) { if (!editor) return; if (profile === "generic") { setEditor({ ...editor, image_parameter_profile: "generic" }); return; } setEditor({ ...editor, ...imagePresets[profile] }); setShowAdvanced(false); } function saveEditor() { if (!editor) return; if (!editor.name.trim() || !editor.model_id.trim() || !editor.base_url.trim()) { setEditorError("请填写显示名称、模型 ID 和 API Base URL。"); return; } if (!editor.api_key?.trim() && !editor.api_key_configured) { setEditorError("请填写 API Key。"); return; } if (editor.category === "multimodal" && editor.capabilities.length === 0) { setEditorError("请至少选择一种多模态能力。"); return; } const normalized = { ...editor, name: editor.name.trim(), model_id: editor.model_id.trim(), base_url: editor.base_url.trim(), capabilities: editor.category === "language" ? ["orchestration" as const] : editor.capabilities, }; const exists = pool.some((item) => item.id === editor.id); replacePool(exists ? pool.map((item) => item.id === editor.id ? normalized : item) : [...pool, normalized]); setEditor(null); setEditorError(""); } function removeModel(model: ModelPoolItem) { if (!window.confirm(`确定从模型池删除“${model.name}”吗?保存前仍可刷新页面撤销。`)) return; replacePool(pool.filter((item) => item.id !== model.id)); if (draft.orchestrator_model_id === model.id) onChange("orchestrator_model_id", ""); if (draft.spatial_model_id === model.id) onChange("spatial_model_id", ""); if (draft.image_model_id === model.id) onChange("image_model_id", ""); } function updateCapability(capability: ModelCapability, checked: boolean) { if (!editor) return; const next = checked ? [...new Set([...editor.capabilities, capability])] : editor.capabilities.filter((item) => item !== capability); setEditor({ ...editor, capabilities: next }); } return (
模型逐个验证,错误逐个定位

同一聚合平台可以添加多个模型,也可以为不同模型配置不同平台和密钥。

{languageModels.length} 个大语言模型 {multimodalModels.length} 个多模态模型
{editor ? (

{pool.some((item) => item.id === editor.id) ? "编辑模型" : "添加模型"}

“验证连接”检查地址、密钥和模型列表;部分媒体模型不会出现在列表中,最终以“试生成”的端到端结果为准。

{editor.category === "multimodal" ? ( ) : null}
{editor.category === "multimodal" ? (
模型能力 {(["spatial_understanding", "image_generation", "image_editing"] as ModelCapability[]).map((capability) => ( ))}
) : null} {showAdvanced ? (
{([ ["models_path", "模型列表路径"], ["chat_path", "对话路径"], ["image_generation_path", "生图路径"], ["image_edit_path", "图片编辑路径"], ["image_status_path", "任务查询路径"], ] as const).map(([key, label]) => ( ))}
) : null} {editorError ?

{editorError}

: null}
) : null} { setEditor(newModel("language")); setShowAdvanced(false); }} onEdit={(model) => { setEditor({ ...model, api_key: "" }); setShowAdvanced(false); }} onRemove={removeModel} onTest={onTest} onTrial={onTrial} /> { setEditor(newModel("multimodal")); setShowAdvanced(false); }} onEdit={(model) => { setEditor({ ...model, api_key: "" }); setShowAdvanced(false); }} onRemove={removeModel} onTest={onTest} onTrial={onTrial} />

工作流模型路由

总调度必须手动指定。空间理解和生图可以交给总调度自动选择,也可以固定模型。

总调度模型

只显示模型池中的大语言模型。

onChange("spatial_routing_mode", value)} onModelChange={(value) => onChange("spatial_model_id", value)} /> onChange("image_routing_mode", value)} onModelChange={(value) => onChange("image_model_id", value)} />
); } type ModelGroupProps = { title: string; description: string; category: ModelCategory; models: ModelPoolItem[]; tests: Record; busy: string | null; onAdd: () => void; onEdit: (model: ModelPoolItem) => void; onRemove: (model: ModelPoolItem) => void; onTest: (modelId: string) => void; onTrial: (modelId: string) => void; }; function ModelGroup({ title, description, category, models, tests, busy, onAdd, onEdit, onRemove, onTest, onTrial }: ModelGroupProps) { return (
{category === "language" ? : }

{title}

{description}

{models.length === 0 ? ( ) : (
{models.map((model) => { const target = `model:${model.id}`; const result = tests[target]; const generationTarget = `generation:${model.id}`; const generationResult = tests[generationTarget]; return (
{result?.ok ? : result ? : }
{model.name}{!model.enabled ? 已停用 : null}
{model.model_id} {providerLabels[model.provider] ?? model.provider}
{model.capabilities.map((capability) => {capabilityLabels[capability]})}
{model.capabilities.includes("image_generation") ? ( ) : null}
{result ?

{result.message}

:

尚未验证模型 ID。

} {generationResult ?

{generationResult.message}

: null}
); })}
)}
); } type RouteControlProps = { label: string; description: string; mode: string; selectedId: string; models: ModelPoolItem[]; onModeChange: (value: string) => void; onModelChange: (value: string) => void; }; function RouteControl({ label, description, mode, selectedId, models, onModeChange, onModelChange }: RouteControlProps) { return (
{label}

{description}

{mode === "manual" ? ( ) :

仅从已启用且测试通过的候选模型中选择。

}
); } 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 ( ); }