diff --git a/src/pages/compute/models.tsx b/src/pages/compute/models.tsx index 73e6e8d..1d2fda7 100644 --- a/src/pages/compute/models.tsx +++ b/src/pages/compute/models.tsx @@ -10,10 +10,24 @@ import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; -import { computeAction, useComputeList, fmtInt, fmtTime } from "./useCompute"; +import { computeProxy } from "@/api/resources"; +import { computeAction, useComputeList, fmtTime } from "./useCompute"; interface Row { [k: string]: any } -const blank: Row = { model_name: "", description: "", icon: "", tags: "", endpoints: "", status: 1, sync_official: 1, vendor_id: 0 }; +const blank: Row = { + model_name: "", name: "", actual_model: "", vendor_id: 0, group: "default", + billing_unit: "/百万tokens", cache_hit_price: 0, input_price: 0, output_price: 0, + vision_support: false, tags: "", endpoints: "", description: "", icon: "", status: 1, sync_official: 1, +}; + +function vendorModels(vendor: Row | undefined): string[] { + const m = vendor?.models; + if (Array.isArray(m)) return m.filter(Boolean); + if (typeof m === "string") { + try { const a = JSON.parse(m); return Array.isArray(a) ? a.filter(Boolean) : []; } catch { return []; } + } + return []; +} export default function ModelsPage() { const [status, setStatus] = React.useState("1"); @@ -26,25 +40,28 @@ export default function ModelsPage() { async function toggle(r: Row) { setBusy(true); setMsg(null); const out = await computeAction("PUT", "models", { ...r, id: r.id, status: r.status === 1 ? 0 : 1 }); - setMsg(out.ok ? `模型 ${r.model_name} 已${r.status === 1 ? "停用" : "启用"}` : `操作失败:${out.message}`); + setMsg(out.ok ? `模型 ${r.name || r.model_name} 已${r.status === 1 ? "停用" : "启用"}` : `操作失败:${out.message}`); setBusy(false); res.reload(); } async function del(r: Row) { setBusy(true); setMsg(null); const out = await computeAction("DELETE", `models/${r.id}`); - setMsg(out.ok ? `已删除 ${r.model_name}` : `删除失败:${out.message}`); + setMsg(out.ok ? `已删除 ${r.name || r.model_name}` : `删除失败:${out.message}`); setBusy(false); res.reload(); } const columns: Column[] = [ { key: "id", header: "ID", cell: (r) => {r.id} }, - { key: "model_name", header: "模型名", cell: (r) => {r.model_name} }, - { key: "tags", header: "标签", cell: (r) => r.tags || "-" }, - { key: "vendor_id", header: "供应商", cell: (r) => r.vendor_id ? `#${fmtInt(r.vendor_id)}` : "-" }, - { key: "endpoints", header: "接入端点", cell: (r) => {r.endpoints || "-"} }, - { key: "matched_count", header: "匹配通道", cell: (r) => fmtInt(r.matched_count) }, + { key: "model_name", header: "模型", cell: (r) => ( +
{r.name || r.model_name}
{r.model_name}
+ ) }, + { key: "actual_model", header: "实际模型", cell: (r) => r.actual_model || r.model_name }, + { key: "group", header: "分组", cell: (r) => r.group || "default" }, + { key: "input_price", header: "输入(¥)", cell: (r) => r.input_price ?? "-" }, + { key: "output_price", header: "输出(¥)", cell: (r) => r.output_price ?? "-" }, + { key: "vision_support", header: "视觉", cell: (r) => (r.vision_support ? 支持 : ) }, { key: "status", header: "状态", cell: (r) => {r.status === 1 ? "启用" : "停用"} }, - { key: "updated", header: "更新时间", cell: (r) => fmtTime(r.updated_time) }, + { key: "updated", header: "更新", cell: (r) => fmtTime(r.updated_time) }, { key: "actions", header: "操作", cell: (r) => (
@@ -60,7 +77,7 @@ export default function ModelsPage() { return (
- {msg} @@ -94,18 +111,34 @@ export default function ModelsPage() { ); } -function ModelFormDialog({ open, row, onClose, onDone }: { - open: boolean; row: Row | null; onClose: () => void; onDone: () => void; -}) { +function ModelFormDialog({ open, row, onClose, onDone }: { open: boolean; row: Row | null; onClose: () => void; onDone: () => void }) { const [f, setF] = React.useState(blank); + const [vendors, setVendors] = React.useState([]); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); - React.useEffect(() => { if (open) { setF(row?.id ? blank : { ...blank, ...row }); setErr(null); } }, [open, row]); + const vendor = vendors.find((v) => v.id === Number(f.vendor_id)); + const avail = vendorModels(vendor); const set = (k: string, v: any) => setF((p) => ({ ...p, [k]: v })); + React.useEffect(() => { + if (open) { + setF(row?.id ? { ...blank, ...row, model_name: row.model_name ?? "", name: row.name ?? "" } : blank); + setErr(null); + computeProxy<{ items: Row[] }>("GET", "vendors", undefined, { p: 1, page_size: 100 }) + .then((r: any) => setVendors((r?.body?.data?.items) ?? [])) + .catch(() => setVendors([])); + } + }, [open, row]); + async function save() { setBusy(true); setErr(null); - const body = { ...f, id: row?.id, vendor_id: Number(f.vendor_id) || 0, status: Number(f.status ?? 1), sync_official: Number(f.sync_official ?? 1) }; + const body: any = { + ...f, id: row?.id, vendor_id: Number(f.vendor_id) || 0, + input_price: Number(f.input_price) || 0, output_price: Number(f.output_price) || 0, + cache_hit_price: Number(f.cache_hit_price) || 0, status: Number(f.status ?? 1), + vision_support: !!f.vision_support, actual_model: f.actual_model || f.model_name, + }; + delete body.created_time; delete body.updated_time; delete body.matched_count; const out = row?.id ? await computeAction("PUT", "models", body) : await computeAction("POST", "models", body); setBusy(false); if (out.ok) onDone(); else setErr(out.message || "保存失败"); @@ -113,20 +146,39 @@ function ModelFormDialog({ open, row, onClose, onDone }: { return ( !o && onClose()}> - + {row?.id ? "编辑模型" : "新增模型"}
- set("model_name", e.target.value)} placeholder="gpt-4o" /> - set("tags", e.target.value)} placeholder="chat,vision" /> - set("vendor_id", e.target.value)} /> - set("endpoints", e.target.value)} placeholder="gpt-4o,chatgpt-4o" /> + + + + set("name", e.target.value)} placeholder="DeepSeek Chat" /> + set("model_name", e.target.value)} placeholder="deepseek-chat" /> + + set("actual_model", e.target.value)} placeholder="选择或输入上游模型名" /> + + {avail.map((m) => + + set("group", e.target.value)} /> + set("billing_unit", e.target.value)} /> + set("input_price", e.target.value)} /> + set("output_price", e.target.value)} /> + set("cache_hit_price", e.target.value)} /> + set("tags", e.target.value)} /> set("description", e.target.value)} /> + -
{err &&

{err}

} diff --git a/src/pages/compute/vendors.tsx b/src/pages/compute/vendors.tsx index aa6c726..a3d2b3c 100644 --- a/src/pages/compute/vendors.tsx +++ b/src/pages/compute/vendors.tsx @@ -97,11 +97,23 @@ function VendorFormDialog({ open, row, onClose, onDone }: { open: boolean; row: setBusy(true); setErr(null); const body = { ...f, id: row?.id, status: Number(f.status ?? 1) }; delete body.created_time; delete body.updated_time; + // models: 逗号/换行分隔 → JSON 数组 + body.models = Array.isArray(f.models) + ? f.models + : String(f.models || "").split(/[,\n]/).map((s) => s.trim()).filter(Boolean); const out = row?.id ? await computeAction("PUT", "vendors", body) : await computeAction("POST", "vendors", body); setBusy(false); if (out.ok) onDone(); else setErr(out.message || "保存失败"); } + function parseModels(v: any): string { + if (Array.isArray(v)) return v.join(", "); + if (typeof v === "string") { + try { const a = JSON.parse(v); return Array.isArray(a) ? a.join(", ") : v; } catch { return v; } + } + return ""; + } + return ( !o && onClose()}> @@ -109,6 +121,8 @@ function VendorFormDialog({ open, row, onClose, onDone }: { open: boolean; row:
set("name", e.target.value)} /> set("icon", e.target.value)} /> + + set("models", e.target.value)} placeholder="deepseek-chat, deepseek-reasoner" /> set("description", e.target.value)} />