// 模型管理 — 按厂商分级 · 紧凑列表 · 接入真实 /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 (
0 ? "1px solid var(--line-1)" : "none", gap: 10, alignItems: "center", opacity: isBusy ? 0.5 : (cred.is_active ? 1 : 0.78), }}> {cred.name} {modelName && ( {modelName} )} {priceLabel && ( {priceLabel} )} {cred.base_url && ( {cred.base_url.replace(/^https?:\/\//, "")} )} KEY · {cred.api_key || "未配置"} {stText}
handleActivate(cred)} title={cred.is_active ? "已激活(同 provider 下唯一)" : "点击激活"} style={{ width: 28, height: 15, background: cred.is_active ? "var(--cyan)" : "var(--bg-3)", borderRadius: 8, position: "relative", cursor: cred.is_active ? "default" : "pointer", flexShrink: 0, }}>
); }; // ─── 添加表单 ─── const addForm = (spec) => (
名称 * setForm({ ...form, name: e.target.value })} style={{ flex: 1, padding: "6px 10px", fontSize: 12, border: "1px solid var(--line-1)", borderRadius: 4, background: "white", color: "var(--fg-0)" }} />
{spec.fields.map(f => { const isModelDropdown = f.key === "model_name" && (spec.models || []).length > 0; return (
{f.label}{f.required && *} {isModelDropdown ? ( ) : ( setForm({ ...form, [f.key]: e.target.value })} type={f.type === "secret" ? "password" : "text"} style={{ flex: 1, padding: "6px 10px", fontSize: 12, border: "1px solid var(--line-1)", borderRadius: 4, background: "white", color: "var(--fg-0)", fontFamily: f.type === "secret" ? "ui-monospace, SF Mono, Menlo, monospace" : "inherit" }} /> )}
); })} {/* 自定义模型时露出一个 text input 让用户填模型 id */} {form.__custom_model__ && (spec.models || []).length > 0 && (
自定义 id setForm({ ...form, model_name: e.target.value })} style={{ flex: 1, padding: "6px 10px", fontSize: 12, border: "1px solid var(--line-1)", borderRadius: 4, background: "white", color: "var(--fg-0)" }} />
)}
); return ( <> {totalVendors} 厂商 · {totalCreds} 凭据 · {totalActive} 已激活 } />
{/* 左:消耗 + 预算 + 调用约定 */}
今日消耗
¥1,284.42
▲ +12.4%

{[ { l: "硅基流动", v: "¥820", pct: 64, col: "purple" }, { l: "Gemini", v: "¥210", pct: 16, col: "cyan" }, { l: "可灵", v: "¥156", pct: 12, col: "green" }, { l: "豆包", v: "¥98", pct: 8, col: "orange" }, ].map((r, i) => (
{r.l} {r.v}
))}
预算与告警
日预算 ¥1,284 / ¥2,000
月预算 ¥28,412 / ¥50,000
调用约定
业务方调用时显式指定 provider + name,系统不做自动 fallback。同一 provider 下只有 1 个凭据可被激活。
{/* 中:按厂商分组的紧凑列表 */}
{loading && (
载入中…
)} {error && (
数据载入失败:{error}
)} {!loading && providersSorted.map((spec) => { const meta = vendorMeta[spec.key] || { short: spec.label.slice(0, 1), bg: "#888" }; const credsForVendor = credByProvider[spec.key] || []; const onlineCount = credsForVendor.filter(c => c.is_active).length; return (
{meta.short}
{spec.label} {spec.kind === "video_api" ? "视频 API" : spec.kind === "multimodal_llm" ? "多模态 LLM" : spec.kind === "datasource" ? "数据源" : spec.kind} {credsForVendor.length} 凭据{credsForVendor.length > 0 ? ` · ${onlineCount} 已激活` : ""} {(spec.models || []).length > 0 && ( <> )}
{openCatalog[spec.key] && (spec.models || []).length > 0 && (
{spec.models.map((m, mi) => (
0 ? "1px solid var(--line-1)" : "none", gap: 10, alignItems: "center", fontSize: 11, }}> {m.label} {m.id} {formatModelPrice(m)} {m.notes && {m.notes}}
))}
)}
{credsForVendor.map((c, idx) => credRow(c, idx))} {credsForVendor.length === 0 && adding !== spec.key && (
(暂无凭据)
)} {adding === spec.key ? addForm(spec) : (
startAdd(spec.key)} style={{ padding: "9px 12px", borderTop: credsForVendor.length > 0 ? "1px solid var(--line-1)" : "none", color: "var(--fg-3)", fontSize: 11.5, cursor: "pointer", display: "flex", alignItems: "center", gap: 6, }}> 添加 {spec.label} 凭据
)}
); })}
); }; Object.assign(window, { ModelsPage });