// 模型管理 — 按厂商分级 · 紧凑列表 · 接入真实 /api/providers + /api/credentials // 按 ModelSpec 拼一个简短的价格标签(用于下拉 option 后缀 + 凭据行 chip) function formatModelPrice(model) { if (!model) return ""; const cur = model.currency === "USD" ? "$" : "¥"; if (model.kind === "chat" || model.kind === "multimodal") { const ip = model.input_price, op = model.output_price; if (ip != null && op != null) { return `${cur}${ip}/${cur}${op} ·1M tok`; } } if (model.kind === "video_t2v" || model.kind === "video_i2v") { return `${cur}${model.price}/s`; } if (model.kind === "image") { return `${cur}${model.price}/张`; } if (model.kind === "asr") { return `${cur}${model.price}/min`; } return ""; } const ModelsPage = () => { const [providers, setProviders] = React.useState([]); // [{key,label,kind,fields}] const [credentials, setCredentials] = React.useState([]); // [{id,provider,name,api_key,...}] const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(""); const [adding, setAdding] = React.useState(null); // provider key under add const [form, setForm] = React.useState({}); const [saving, setSaving] = React.useState(false); const [busyId, setBusyId] = React.useState(0); const [openCatalog, setOpenCatalog] = React.useState({}); // {providerKey: true} // 厂商展示元数据(key 对应 PROVIDERS 的 key) const vendorMeta = { siliconflow: { short: "硅", bg: "#5046E4" }, gemini: { short: "G", bg: "#4285F4" }, openai_compat: { short: "OC", bg: "#10A37F" }, openai_multimodal: { short: "OM", bg: "#10A37F" }, kling: { short: "可", bg: "#FF5043" }, seedance: { short: "豆", bg: "#1664FF" }, minimax: { short: "Mi", bg: "#3D7BFE" }, runway: { short: "R", bg: "#7C3AED" }, luma: { short: "L", bg: "#111111" }, pika: { short: "P", bg: "#FF4D6D" }, vidu: { short: "V", bg: "#7C3AED" }, custom_api: { short: "*", bg: "#888888" }, douyin_hub: { short: "抖", bg: "#000000" }, douyin_cookie: { short: "C", bg: "#111111" }, }; // 排序优先级:硅基流动 → 多模态 LLM → 视频 API → 数据源 → 其他 const providerOrder = [ "siliconflow", "gemini", "openai_multimodal", "openai_compat", "kling", "seedance", "minimax", "runway", "luma", "pika", "vidu", "custom_api", "douyin_hub", "douyin_cookie", ]; // ─── 数据装载 ─── React.useEffect(() => { reload(); }, []); async function reload() { setLoading(true); setError(""); try { const [p, c] = await Promise.all([API.providers.list(), API.credentials.list()]); setProviders(p.items || []); setCredentials(c.items || []); } catch (e) { setError(String(e.message || e)); } finally { setLoading(false); } } // ─── 操作 ─── async function handleActivate(cred) { if (cred.is_active) return; setBusyId(cred.id); try { await API.credentials.activate(cred.id); await reload(); } catch (e) { alert("激活失败:" + (e.message || e)); } finally { setBusyId(0); } } async function handleDelete(cred) { if (!confirm(`删除凭据「${cred.name}」?此操作不可撤销。`)) return; setBusyId(cred.id); try { await API.credentials.delete(cred.id); await reload(); } catch (e) { alert("删除失败:" + (e.message || e)); } finally { setBusyId(0); } } async function handleTest(cred) { setBusyId(cred.id); try { const r = await API.credentials.test(cred.id); alert(`${r.ok ? "✓ 测试通过" : "✗ 测试失败"}\n${r.message || ""}${r.latency_ms ? ` (${r.latency_ms}ms)` : ""}`); await reload(); } catch (e) { alert("测试失败:" + (e.message || e)); } finally { setBusyId(0); } } function startAdd(providerKey) { setAdding(providerKey); setForm({}); } function cancelAdd() { setAdding(null); setForm({}); } async function submitAdd(spec) { const name = (form.name || "").trim(); if (!name) { alert("请填写凭据名称"); return; } for (const f of spec.fields) { if (f.required && !form[f.key]) { alert(`「${f.label}」是必填字段`); return; } } setSaving(true); try { const body = { provider: spec.key, name }; for (const f of spec.fields) { const v = form[f.key]; if (v !== undefined && v !== "") body[f.key] = v; } await API.credentials.create(body); cancelAdd(); await reload(); } catch (e) { alert("添加失败:" + (e.message || e)); } finally { setSaving(false); } } // ─── 分组 ─── const credByProvider = {}; for (const c of credentials) { if (!credByProvider[c.provider]) credByProvider[c.provider] = []; credByProvider[c.provider].push(c); } // 显示所有 providers,不论是否有凭据("+ 添加" 永远可见) const providersSorted = [...providers].sort((a, b) => { const ai = providerOrder.indexOf(a.key); const bi = providerOrder.indexOf(b.key); return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi); }); const totalCreds = credentials.length; const totalActive = credentials.filter(c => c.is_active).length; const totalVendors = providers.length; // provider key → models[] 反查表(用于在凭据行展示价格 chip) const modelsByProvider = React.useMemo(() => { const m = {}; for (const p of providers) { m[p.key] = {}; for (const md of (p.models || [])) m[p.key][md.id] = md; } return m; }, [providers]); // ─── 单行渲染 ─── const credRow = (cred, idx) => { const isErr = cred.last_test_status === "error"; const stCol = isErr ? "pink" : (cred.is_active ? "green" : "fg-2"); const stText = isErr ? "异常" : (cred.is_active ? "在线" : "待机"); const isBusy = busyId === cred.id; const modelName = cred.extra && cred.extra.model_name; const modelSpec = modelName && modelsByProvider[cred.provider] ? modelsByProvider[cred.provider][modelName] : null; const priceLabel = formatModelPrice(modelSpec); return (