import { useCallback, useEffect, useState } from "react"; import { Server, X } from "lucide-react"; import { adminApi } from "../../api"; import { Pager } from "../../components/pager"; import { SystemLoading } from "../../components/loading"; import { CustomSelect } from "../../components/custom-select"; import type { AdminModel, AdminProvider } from "../../types"; import { pts } from "../stage-config"; function sortAdminModels(list: AdminModel[]): AdminModel[] { // 默认最前,启用次之,停用靠后;同档按能力、供应商 return [...list].sort((a, b) => { if (Boolean(a.is_default) !== Boolean(b.is_default)) return a.is_default ? -1 : 1; if ((a.status === "active") !== (b.status === "active")) return a.status === "active" ? -1 : 1; const cap = String(a.capability || "").localeCompare(String(b.capability || "")); if (cap !== 0) return cap; return String(a.provider_name || a.provider || "").localeCompare(String(b.provider_name || b.provider || "")); }); } type Notify = (type: "success" | "error" | "info", text: string) => void; const PAGE_SIZE = 10; const CAPABILITIES = ["text", "image", "video", "vision", "audio", "export"]; function statusPill(active: boolean) { return active ? 启用 : 停用; } const EMPTY_PROVIDER = { id: "", name: "", display_name: "", base_url: "", api_key: "", status: "active" }; const VIDEO_RES_OPTIONS = ["480p", "720p", "1080p", "4k"]; type VideoTier = { resolution: string; points_per_second: string; points_per_second_with_ref: string }; type ModelForm = { id: string; provider: string; name: string; display_name: string; capability: string; endpoint: string; unit_price: string; status: string; durationsText: string; videoTiers: VideoTier[]; }; const EMPTY_MODEL: ModelForm = { id: "", provider: "", name: "", display_name: "", capability: "text", endpoint: "", unit_price: "", status: "active", durationsText: "4,5,6,8,10,12,15", videoTiers: [{ resolution: "480p", points_per_second: "20", points_per_second_with_ref: "" }], }; function readModelForm(m: AdminModel): ModelForm { const meta = (m.metadata || {}) as Record; const caps = (meta.capabilities || {}) as Record; const pricing = (meta.points_pricing || {}) as Record; const durations = Array.isArray(caps.durations) ? caps.durations.map(String) : []; const tiersRaw = Array.isArray(pricing.tiers) ? pricing.tiers : []; const videoTiers: VideoTier[] = tiersRaw .filter((row): row is Record => !!row && typeof row === "object") .map((row) => ({ resolution: String(row.resolution || ""), points_per_second: String(row.points_per_second ?? ""), points_per_second_with_ref: String(row.points_per_second_with_ref ?? ""), })); let unit = m.unit_price || ""; if (m.capability === "image" && pricing.points_per_image != null) unit = String(pricing.points_per_image); if ((m.capability === "text" || m.capability === "vision") && pricing.points_per_call != null) { unit = String(pricing.points_per_call); } return { id: m.id, provider: m.provider, name: m.name, display_name: m.display_name, capability: m.capability, endpoint: m.endpoint, unit_price: unit, status: m.status, durationsText: durations.length ? durations.join(",") : "4,5,6,8,10,12,15", videoTiers: videoTiers.length ? videoTiers : [{ resolution: "480p", points_per_second: "", points_per_second_with_ref: "" }], }; } function buildMetadata(form: ModelForm): Record { if (form.capability === "image") { const pts = Number(form.unit_price || 0); return { points_pricing: { mode: "per_image", points_per_image: Number.isFinite(pts) ? pts : 0 } }; } if (form.capability === "text" || form.capability === "vision") { const pts = Number(form.unit_price || 0); return { points_pricing: { mode: "per_call", points_per_call: Number.isFinite(pts) ? pts : 0 } }; } if (form.capability === "video") { const durations = form.durationsText .split(/[,,\s]+/) .map((x) => Number(x)) .filter((n) => Number.isFinite(n) && n > 0); const tiers = form.videoTiers .filter((row) => row.resolution && row.points_per_second !== "") .map((row) => { const item: Record = { resolution: row.resolution, points_per_second: Number(row.points_per_second), }; if (row.points_per_second_with_ref !== "") { item.points_per_second_with_ref = Number(row.points_per_second_with_ref); } return item; }); return { capabilities: { resolutions: tiers.map((t) => String(t.resolution)), durations, operations: ["video_generate"], }, points_pricing: { mode: "per_second", tiers }, }; } return {}; } export function AdminModelsPage({ notify }: { notify: Notify }) { const [providers, setProviders] = useState([]); const [models, setModels] = useState([]); const [modelPage, setModelPage] = useState(1); const [loading, setLoading] = useState(true); const [provModal, setProvModal] = useState(null); const [modelModal, setModelModal] = useState(null); const [saving, setSaving] = useState(false); const load = useCallback(async () => { setLoading(true); try { const [ps, ms] = await Promise.all([adminApi.providers(), adminApi.models()]); setProviders(ps); setModels(sortAdminModels(ms)); } catch { notify("error", "加载模型供应商失败"); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { void load(); }, [load]); async function toggleProvider(p: AdminProvider) { try { await adminApi.updateProvider(p.id, { status: p.status === "active" ? "disabled" : "active" }); notify("success", "供应商状态已更新"); await load(); } catch { notify("error", "操作失败"); } } async function toggleModel(m: AdminModel) { try { await adminApi.updateModel(m.id, { status: m.status === "active" ? "disabled" : "active" }); notify("success", "模型状态已更新"); await load(); } catch { notify("error", "操作失败"); } } async function setDefault(m: AdminModel) { try { await adminApi.setDefaultModel(m.id); notify("success", `已设为 ${m.capability} 默认模型`); await load(); } catch { notify("error", "设默认失败"); } } async function delModel(m: AdminModel) { try { await adminApi.deleteModel(m.id); notify("success", "模型已删除"); await load(); } catch { notify("error", "删除失败"); } } async function saveProvider() { if (!provModal || saving) return; if (!provModal.name.trim() || !provModal.display_name.trim()) { notify("error", "请填写名称"); return; } setSaving(true); try { const payload: Record = { display_name: provModal.display_name, base_url: provModal.base_url, status: provModal.status }; if (provModal.api_key) payload.api_key = provModal.api_key; if (provModal.id) { await adminApi.updateProvider(provModal.id, payload); } else { await adminApi.createProvider({ name: provModal.name, display_name: provModal.display_name, base_url: provModal.base_url, api_key: provModal.api_key || undefined, status: provModal.status }); } notify("success", "供应商已保存"); setProvModal(null); await load(); } catch (e) { notify("error", e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } } async function saveModel() { if (!modelModal || saving) return; if (!modelModal.provider || !modelModal.name.trim()) { notify("error", "请选择供应商并填写模型名"); return; } if (modelModal.capability === "video") { const ok = modelModal.videoTiers.some((row) => row.resolution && row.points_per_second !== ""); if (!ok) { notify("error", "视频模型请至少添加一档分辨率和每秒积分"); return; } } setSaving(true); try { const metadata = buildMetadata(modelModal); if (modelModal.id) { await adminApi.updateModel(modelModal.id, { display_name: modelModal.display_name, endpoint: modelModal.endpoint, unit_price: modelModal.unit_price || "0", status: modelModal.status, metadata, }); } else { await adminApi.createModel({ provider: modelModal.provider, name: modelModal.name, display_name: modelModal.display_name || modelModal.name, capability: modelModal.capability, endpoint: modelModal.endpoint, unit_price: modelModal.unit_price || "0", status: modelModal.status, metadata, } as Parameters[0] & { metadata: Record }); } notify("success", "模型已保存"); setModelModal(null); await load(); } catch (e) { notify("error", e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } } return ( <>

模型供应商

{providers.length} 供应商 · {models.length} 模型 · 热插拔中转站 · 定价 · 设默认
{loading ? ( ) : ( <>
供应商
{providers.map((p) => ( ))}
名称显示名Base URL密钥模型数状态操作
{p.name} {p.display_name} {p.base_url || —} {p.has_api_key ? 已配 : 无} {p.model_count} {statusPill(p.status === "active")}
模型
{models.slice((modelPage - 1) * PAGE_SIZE, modelPage * PAGE_SIZE).map((m) => ( ))}
供应商模型能力单价(积分/次)状态默认操作
{m.provider_name} {m.display_name || m.name} {m.capability} {pts(m.unit_price)} 积分 {statusPill(m.status === "active")} {m.is_default ? 默认 : —} {!m.is_default && }
)} {provModal && (
++
{provModal.id ? "编辑供应商" : "新建供应商"}
setProvModal((p) => p && ({ ...p, name: e.target.value }))} />
setProvModal((p) => p && ({ ...p, display_name: e.target.value }))} />
setProvModal((p) => p && ({ ...p, base_url: e.target.value }))} />
setProvModal((p) => p && ({ ...p, api_key: e.target.value }))} />
)} {modelModal && (
++
{modelModal.id ? "编辑模型" : "新建模型"}
setModelModal((m) => m && ({ ...m, provider: next }))} options={providers.map((p) => ({ value: p.id, label: p.display_name }))} />
setModelModal((m) => m && ({ ...m, name: e.target.value }))} />
setModelModal((m) => m && ({ ...m, capability: next }))} options={CAPABILITIES.map((c) => ({ value: c, label: c }))} />
setModelModal((m) => m && ({ ...m, display_name: e.target.value }))} />
setModelModal((m) => m && ({ ...m, endpoint: e.target.value }))} />
{modelModal.capability === "image" ? (
setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
老板填写:用该模型出 1 张图扣多少积分
) : null} {modelModal.capability === "text" || modelModal.capability === "vision" ? (
setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
老板填写:调用 1 次扣多少积分
) : null} {modelModal.capability === "video" ? ( <>
setModelModal((m) => m && ({ ...m, durationsText: e.target.value }))} />
每个分辨率一档;含视频参考可另填更高积分,不填则同普通价
{modelModal.videoTiers.map((tier, index) => (
setModelModal((m) => { if (!m) return m; const videoTiers = m.videoTiers.map((row, i) => i === index ? { ...row, resolution: next } : row); return { ...m, videoTiers }; })} options={VIDEO_RES_OPTIONS.map((r) => ({ value: r, label: r }))} />
setModelModal((m) => { if (!m) return m; const videoTiers = m.videoTiers.map((row, i) => i === index ? { ...row, points_per_second: e.target.value } : row); return { ...m, videoTiers }; })} />
setModelModal((m) => { if (!m) return m; const videoTiers = m.videoTiers.map((row, i) => i === index ? { ...row, points_per_second_with_ref: e.target.value } : row); return { ...m, videoTiers }; })} />
))}
) : null} {!["image", "text", "vision", "video"].includes(modelModal.capability) ? (
setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
) : null}
)} ); }