import { Alert, App, Button, Drawer, Form, Input, InputNumber, Modal, Segmented, Select, Switch, Table, Tag } from "antd"; import type { FormInstance } from "antd"; import type { ColumnsType } from "antd/es/table"; import { FlaskConical, GitBranch, Layers3, Pencil, Plus, Search } from "lucide-react"; import { useDeferredValue, useEffect, useMemo, useState, type ReactNode } from "react"; import { PaginationBar } from "@/components/layout/workspace-page"; import { ModelIconPicker, ModelLogo } from "@/components/model-logo"; import { CapabilityCardPicker } from "@/components/model-protocol-picker"; import { formatCredits } from "@/constant/credits"; import { AdminPageFrame } from "@/pages/admin/components/admin-shell"; import { AdminDataTable, AdminFilterChip, AdminRowActions, AdminStatusBadge, AdminTableEmpty } from "@/pages/admin/components/admin-ui"; import { listAdminChannels } from "@/services/api/auth"; import { listAdminChannelModels, type ChannelModel } from "@/services/api/wallet"; import { createAdminLogicalModel, listAdminLogicalModels, simulateAdminLogicalModel, updateAdminLogicalModel, type AdminLogicalModel, type CapabilitySpec, type LogicalModelMutation, type ModelRequestIntent, type RouteSimulationResult, } from "@/services/api/logical-models"; import { CapabilityRequestEditor, CapabilityScopeEditor, CapabilitySummary, DefaultOptionsEditor, capabilityLabel, capabilitySpecFromChannelModel, capabilitySourceError, emptyCapabilitySpec, mergeCapabilitySpecs, normalizeCapabilitySpecForSources, operationLabel, sanitizeDefaults, type CapabilityKind, } from "./model-routing-capabilities"; type RouteRuleRow = { channelModelId: string; enabled: boolean; priority: number; weight: number }; type LogicalModelFormValues = { code: string; name: string; icon: string; description: string; capability: CapabilityKind; enabled: boolean; sortOrder: number; pricePolicy: LogicalModelMutation["pricePolicy"]; billingMode: LogicalModelMutation["billingMode"]; unitPriceMicrocredits: number; inputPriceMicrocredits: number; outputPriceMicrocredits: number; cachedPriceMicrocredits: number; capabilitySpec: CapabilitySpec; defaultOptions: Record; routes: RouteRuleRow[]; }; export default function LogicalModelsPage() { const { message } = App.useApp(); const [keyword, setKeyword] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const deferredKeyword = useDeferredValue(keyword.trim().toLowerCase()); const [loading, setLoading] = useState(true); const [models, setModels] = useState([]); const [channelModels, setChannelModels] = useState([]); const [channelNames, setChannelNames] = useState>({}); const [channelEnabled, setChannelEnabled] = useState>({}); const [editingModel, setEditingModel] = useState(); const [saving, setSaving] = useState(false); const [simulatingModel, setSimulatingModel] = useState(); const [simulationIntent, setSimulationIntent] = useState(); const [simulationResult, setSimulationResult] = useState(); const [simulating, setSimulating] = useState(false); const [modelForm] = Form.useForm(); const modelCapability = Form.useWatch("capability", modelForm) || "image"; const modelRoutes = Form.useWatch("routes", modelForm) || []; const modelCapabilitySpec = Form.useWatch("capabilitySpec", modelForm); const reload = async () => { setLoading(true); try { const [modelResult, firstChannelPage] = await Promise.all([listAdminLogicalModels(), listAdminChannels({ page: 1, limit: 100 })]); const remainingChannelPages = await Promise.all( Array.from({ length: Math.max(0, Math.ceil(firstChannelPage.total / firstChannelPage.limit) - 1) }, (_, index) => listAdminChannels({ page: index + 2, limit: firstChannelPage.limit })), ); const channels = [firstChannelPage, ...remainingChannelPages].flatMap((result) => result.channels); const channelModelResults = await Promise.all(channels.map((channel) => listAdminChannelModels(channel.id))); setModels(modelResult.models); setChannelModels(channelModelResults.flatMap((result) => result.models)); setChannelNames(Object.fromEntries(channels.map((channel) => [channel.id, channel.name]))); setChannelEnabled(Object.fromEntries(channels.map((channel) => [channel.id, channel.enabled !== false]))); } catch (error) { message.error(error instanceof Error ? error.message : "读取前台模型配置失败"); } finally { setLoading(false); } }; useEffect(() => { void reload(); }, []); const filteredModels = useMemo(() => models.filter((item) => !deferredKeyword || [item.name, item.code, item.capability].some((value) => value.toLowerCase().includes(deferredKeyword))), [models, deferredKeyword]); const paginatedModels = useMemo(() => filteredModels.slice((page - 1) * pageSize, page * pageSize), [filteredModels, page, pageSize]); const modelChannelModels = useMemo(() => channelModels.filter((item) => item.capability === modelCapability), [channelModels, modelCapability]); const modelSourceSpecs = useMemo( () => modelRoutes .filter((route) => route.enabled && route.weight > 0) .map((route) => channelModels.find((item) => item.id === route.channelModelId && item.enabled && channelEnabled[item.channelId] !== false)) .map((item) => (item ? capabilitySpecFromChannelModel(item) : undefined)) .filter((item): item is CapabilitySpec => Boolean(item)), [channelEnabled, channelModels, modelRoutes], ); const openModel = (item?: AdminLogicalModel) => { const capability = item?.capability || "image"; modelForm.resetFields(); modelForm.setFieldsValue( item ? logicalModelToForm(item) : { code: "", name: "", icon: "", description: "", capability, enabled: true, sortOrder: models.length, pricePolicy: "unified", billingMode: "fixed_request", unitPriceMicrocredits: 0, inputPriceMicrocredits: 0, outputPriceMicrocredits: 0, cachedPriceMicrocredits: 0, capabilitySpec: emptyCapabilitySpec(capability), defaultOptions: {}, routes: [], }, ); setEditingModel(item || null); }; const saveModel = async () => { const values = await modelForm.validateFields(); if (values.enabled && !values.routes.length) { message.error("请至少添加一条供应线路"); return; } const sourceError = capabilitySourceError(values.capability, modelSourceSpecs, values.capabilitySpec); if (values.enabled && sourceError) { message.error(sourceError); return; } setSaving(true); try { const payload = logicalModelPayload(values, modelSourceSpecs); await (editingModel ? updateAdminLogicalModel(editingModel.id, payload) : createAdminLogicalModel(payload)); setEditingModel(undefined); await reload(); message.success(editingModel ? "前台模型已更新" : "前台模型已创建"); } catch (error) { message.error(error instanceof Error ? error.message : "保存前台模型失败"); } finally { setSaving(false); } }; const toggleModel = async (item: AdminLogicalModel) => { try { await updateAdminLogicalModel(item.id, logicalModelPayload({ ...logicalModelToForm(item), enabled: !item.enabled })); await reload(); message.success(item.enabled ? "前台模型已停用" : "前台模型已启用"); } catch (error) { message.error(error instanceof Error ? error.message : "更新模型状态失败"); } }; const openSimulation = (item: AdminLogicalModel) => { setSimulationIntent({ capability: item.capability, operation: item.capabilitySpec.operations?.[0], inputs: Object.fromEntries(Object.entries(item.capabilitySpec.inputs || {}).map(([name, value]) => [name, value.min])), options: { ...item.defaultOptions }, }); setSimulationResult(undefined); setSimulatingModel(item); }; const runSimulation = async () => { if (!simulatingModel || !simulationIntent) return; setSimulating(true); try { setSimulationResult(await simulateAdminLogicalModel(simulatingModel.id, simulationIntent)); } catch (error) { message.error(error instanceof Error ? error.message : "路由模拟失败"); } finally { setSimulating(false); } }; const modelColumns: ColumnsType = [ { title: "前台模型", dataIndex: "name", render: (_, item) => (
{item.name}
{item.code}
), }, { title: "类型", dataIndex: "capability", width: 90, render: (value: CapabilityKind) => capabilityLabel(value) }, { title: "创作端能力", width: 360, render: (_, item) => }, { title: "供应线路", width: 110, render: (_, item) => (
{item.routes.filter((route) => route.enabled && route.available).length} 条可用
共 {item.routes.length} 条
), }, { title: "用户价格", width: 160, render: (_, item) => logicalPriceLabel(item) }, { title: "状态", width: 130, render: (_, item) => logicalModelStatusTag(item) }, { title: "操作", width: 230, align: "right", render: (_, item) => ( , onClick: () => openModel(item) }} actions={[ { key: "simulate", label: "模拟供应线路匹配", icon: , onClick: () => openSimulation(item) }, { key: "toggle", label: item.enabled ? "停用" : "启用", onClick: () => void toggleModel(item) }, ]} /> ), }, ]; return ( } onClick={() => openModel()}> 新增模型 } > } allowClear value={keyword} onChange={(event) => { setKeyword(event.target.value); setPage(1); }} placeholder="搜索模型名称、代码或能力" className="app-list-search" />} toolbarActiveFilters={keyword ? { setKeyword(""); setPage(1); }} /> : null} toolbarActive={Boolean(keyword)} onReset={() => { setKeyword(""); setPage(1); }} table={{ rowKey: "id", size: "small", loading, pagination: false, columns: modelColumns, dataSource: paginatedModels, scroll: { x: 980 } }} empty={} footer={ { setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} />} /> !saving && setEditingModel(undefined)} rootClassName="admin-drawer" footer={
} >
) => { const capability = changedValues.capability; if (!capability) return; modelForm.setFieldsValue({ routes: [], capabilitySpec: emptyCapabilitySpec(capability), defaultOptions: {}, billingMode: capability === "text" ? "token" : capability === "video" ? "per_second" : "fixed_request", }); }} > {editingModel?.configurationError || editingModel?.availabilityError ? ( ) : null} } title="前台展示">
} title="供应线路">
setSimulatingModel(undefined)} styles={{ body: { maxHeight: "min(72vh, 720px)", overflowY: "auto" } }} footer={[ , , ]} > {simulatingModel && simulationIntent ? (
{simulatingModel.capabilitySpec.operations?.length ? (
); })} ) : null} {!fields.length ?
尚未添加供应线路
: null} ); }} ); } function logicalModelStatusTag(item: AdminLogicalModel) { if (!item.enabled) return ; if (item.configurationError) return ; if (item.availabilityError) return ; if (!item.available) return ; return ; } function PricingFields() { const form = Form.useFormInstance(); const pricePolicy = Form.useWatch("pricePolicy") as LogicalModelMutation["pricePolicy"] | undefined; const billingMode = Form.useWatch("billingMode") as LogicalModelMutation["billingMode"] | undefined; const capability = Form.useWatch("capability") as CapabilityKind | undefined; const modes = [{ label: "按次", value: "fixed_request" }]; if (capability === "video") modes.push({ label: "按秒", value: "per_second" }); if (capability === "text") modes.push({ label: "Token", value: "token" }); const changePolicy = (value: string | number) => { const nextPolicy = value as LogicalModelMutation["pricePolicy"]; if (nextPolicy !== "unified") return; const supportedModes = modes.map((item) => item.value); if (!billingMode || !supportedModes.includes(billingMode)) { form.setFieldValue("billingMode", capability === "text" ? "token" : capability === "video" ? "per_second" : "fixed_request"); } }; return ( <> {pricePolicy === "unified" ? ( <>
{billingMode !== "token" ? ( ) : null}
{billingMode === "token" ? (
) : null} ) : (
用户费用按实际命中的供应线路价格计算。故障切换到更高价格线路时会重新校验余额。
)} ); } function CreditsInput({ value = 0, onChange }: { value?: number; onChange?: (value: number) => void }) { return onChange?.(Math.round((next || 0) * 1_000_000))} />; } function logicalPriceLabel(item: AdminLogicalModel) { if (item.pricePolicy === "channel") return 跟随供应价格; if (item.billingMode === "token") return (
输入 {formatCredits(item.inputPriceMicrocredits)} / 百万
输出 {formatCredits(item.outputPriceMicrocredits)} / 百万
缓存 {formatCredits(item.cachedPriceMicrocredits)} / 百万
); return ( {formatCredits(item.unitPriceMicrocredits)} / {item.billingMode === "per_second" ? "秒" : "次"} ); } function logicalModelToForm(item: AdminLogicalModel): LogicalModelFormValues { return { code: item.code, name: item.name, icon: item.icon || "", description: item.description, capability: item.capability, enabled: item.enabled, sortOrder: item.sortOrder, pricePolicy: item.pricePolicy, billingMode: item.billingMode, unitPriceMicrocredits: item.unitPriceMicrocredits, inputPriceMicrocredits: item.inputPriceMicrocredits, outputPriceMicrocredits: item.outputPriceMicrocredits, cachedPriceMicrocredits: item.cachedPriceMicrocredits, capabilitySpec: item.capabilitySpec, defaultOptions: item.defaultOptions, routes: item.routes.map((route) => ({ channelModelId: route.channelModelId, enabled: route.enabled, priority: route.priority, weight: route.weight })), }; } function logicalModelPayload(values: LogicalModelFormValues, sourceSpecs: CapabilitySpec[] = []): LogicalModelMutation { const capabilitySpec = normalizeCapabilitySpecForSources({ ...values.capabilitySpec, capability: values.capability, version: 1 as const }, sourceSpecs) || emptyCapabilitySpec(values.capability); return { code: values.code.trim(), name: values.name.trim(), icon: values.icon.trim(), description: values.description?.trim() || "", capability: values.capability, enabled: values.enabled, sortOrder: values.sortOrder || 0, pricePolicy: values.pricePolicy, billingMode: values.billingMode, unitPriceMicrocredits: values.unitPriceMicrocredits || 0, inputPriceMicrocredits: values.inputPriceMicrocredits || 0, outputPriceMicrocredits: values.outputPriceMicrocredits || 0, cachedPriceMicrocredits: values.cachedPriceMicrocredits || 0, capabilitySpec, defaultOptions: sanitizeDefaults(capabilitySpec, values.defaultOptions), routes: values.routes.map((route) => ({ ...route, priority: route.priority || 0, weight: route.weight || 0 })), }; } function hasCapabilityRules(spec?: CapabilitySpec) { return Boolean(spec && ((spec.operations?.length || 0) > 0 || Object.keys(spec.inputs || {}).length > 0 || Object.keys(spec.options || {}).length > 0)); } function simulationColumns(): ColumnsType { return [ { title: "供应线路", render: (_, candidate) => `${candidate.channelModelName}(${candidate.channelModelKey})`, }, { title: "优先级", dataIndex: "priority", width: 80 }, { title: "权重", dataIndex: "weight", width: 70 }, { title: "结果", width: 110, render: (_, candidate) => ( {candidate.inPool ? "进入候选池" : candidate.blocked ? "冷却中" : candidate.matched ? "低优先级" : "不匹配"} ), }, { title: "原因", render: (_, candidate) => candidate.reasons?.join(";") || "-" }, ]; }