From c8defa80848b3f063c395f5ca70b782195814801 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Tue, 4 Aug 2026 22:13:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor(admin):=20=E6=A8=A1=E5=9E=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20-=20=E8=83=BD=E5=8A=9B=E4=B8=8E=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E6=94=B9=E4=B8=BA=E5=93=81=E7=89=8C=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E5=8D=95=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/model-picker.tsx | 3 +- .../components/channel-model-manager.tsx | 160 ++++++++++++++++-- 2 files changed, 144 insertions(+), 19 deletions(-) diff --git a/web/src/components/model-picker.tsx b/web/src/components/model-picker.tsx index e0a702240..748bf5585 100644 --- a/web/src/components/model-picker.tsx +++ b/web/src/components/model-picker.tsx @@ -248,7 +248,8 @@ function modelMenuMeta(model: string, capability?: ModelCapability): { descripti export function ModelIcon({ model }: { model: string }) { const icon = resolveModelIcon(modelOptionName(model)); - return icon ? : ; + const monochrome = icon === "/icons/openai.svg" || icon === "/icons/grok.svg"; + return icon ? : ; } export function resolveModelIcon(model: string) { diff --git a/web/src/pages/admin/components/channel-model-manager.tsx b/web/src/pages/admin/components/channel-model-manager.tsx index 8765f2c66..c8bb75e60 100644 --- a/web/src/pages/admin/components/channel-model-manager.tsx +++ b/web/src/pages/admin/components/channel-model-manager.tsx @@ -1,18 +1,22 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, type ReactNode } from "react"; import { App, Button, Drawer, Form, Input, InputNumber, Popconfirm, Segmented, Select, Space, Switch, Table, Tag } from "antd"; import type { ColumnsType } from "antd/es/table"; -import { FlaskConical, Plus, RefreshCw, Search, Trash2 } from "lucide-react"; +import { AudioLines, Check, Film, Flame, FlaskConical, Image, MessageSquareText, Network, Plus, RefreshCw, Search, Sparkles, Trash2 } from "lucide-react"; import { ListToolbar, TableSurface } from "@/components/layout/workspace-page"; -import { SYSTEM_MODEL_PROTOCOL_OPTIONS, modelProtocolCapability, modelProtocolDefinition, modelProtocolLabel, modelProtocolSummary, type ModelProtocol } from "@/lib/model-protocols"; +import { ModelIcon } from "@/components/model-picker"; +import { cn } from "@/lib/utils"; +import { MODEL_PROTOCOLS, modelProtocolCapability, modelProtocolDefinition, modelProtocolLabel, type ModelProtocol, type ModelProtocolDefinition } from "@/lib/model-protocols"; import { createAdminChannelModel, deleteAdminChannelModel, fetchAdminChannelModels, listAdminChannelModels, testAdminChannelModel, updateAdminChannelModel, type ChannelModel } from "@/services/api/wallet"; import type { ModelChannel } from "@/stores/use-config-store"; import { AdminPageFrame } from "./admin-shell"; +type EditableCapability = Exclude; + type FormValues = { modelKey: string; displayName?: string; - capability: ChannelModel["capability"]; + capability: EditableCapability; protocol: ModelProtocol; billingMode: ChannelModel["billingMode"]; unitPrice: number; @@ -36,7 +40,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel: const [form] = Form.useForm(); const billingMode = Form.useWatch("billingMode", form) || "fixed_request"; const modelCapability = Form.useWatch("capability", form); - const modelProtocol = Form.useWatch("protocol", form); + const modelKey = Form.useWatch("modelKey", form) || ""; const reload = async () => { if (!channel) return; @@ -79,7 +83,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel: const startCreate = () => { setEditing(null); - form.setFieldsValue({ modelKey: "", displayName: "", capability: undefined, protocol: undefined, billingMode: "fixed_request", unitPrice: 0, enabled: true }); + form.setFieldsValue({ modelKey: "", displayName: "", capability: "text", protocol: "chat-completion", billingMode: "fixed_request", unitPrice: 0, enabled: true }); setEditorOpen(true); }; @@ -145,13 +149,25 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel: } }; + const handleFormValuesChange = (changed: Partial) => { + if (!changed.capability) return; + if (changed.capability !== "video") form.setFieldValue("billingMode", "fixed_request"); + const current = form.getFieldValue("protocol") as ModelProtocol | undefined; + if (modelProtocolCapability(current) !== changed.capability) { + form.setFieldValue("protocol", MODEL_PROTOCOLS.find((item) => item.capability === changed.capability)?.value); + } + }; + const columns: ColumnsType = [ { title: "模型", render: (_, item) => ( -
-
{item.displayName || item.modelKey}
-
{item.modelKey}
+
+ +
+
{item.displayName || item.modelKey}
+
{item.modelKey}
+
), }, @@ -208,23 +224,19 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel: /> setEditorOpen(false)} styles={{ body: { paddingBottom: 88 } }} extra={editing ? : null}> -
+ - + } placeholder="例如:deepseek-chat、gpt-5、glm-4.5" /> - ({ ...group, options: group.options.filter((item) => modelProtocolCapability(item.value) === modelCapability) }))} /> + + -
-
实际请求预览
-
{modelProtocolSummary(modelProtocol)}
-
@@ -247,6 +259,118 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel: ); } +const capabilityChoices: Array<{ + value: EditableCapability; + label: string; + description: string; + icon: ReactNode; + brands: string[]; +}> = [ + { value: "text", label: "文本", description: "对话与推理", icon: , brands: ["openai", "deepseek", "glm"] }, + { value: "image", label: "图片", description: "生成与编辑", icon: , brands: ["openai", "gemini"] }, + { value: "video", label: "视频", description: "生成与续写", icon: , brands: ["grok", "gemini"] }, + { value: "audio", label: "音频", description: "语音与音效", icon: , brands: ["openai"] }, +]; + +function CapabilityCardPicker({ value, onChange }: { value?: EditableCapability; onChange?: (value: EditableCapability) => void }) { + return ( +
+ {capabilityChoices.map((item) => { + const selected = value === item.value; + return ( + + ); + })} +
+ ); +} + +function ProtocolCardPicker({ capability, value, onChange }: { capability?: EditableCapability; value?: ModelProtocol; onChange?: (value: ModelProtocol) => void }) { + const protocols = MODEL_PROTOCOLS.filter((item) => item.capability === capability); + return ( +
+ {protocols.map((protocol) => { + const selected = value === protocol.value; + return ( + + ); + })} +
+ ); +} + +function ProtocolBrandMark({ protocol }: { protocol: ModelProtocolDefinition }) { + if (protocol.value === "chat-completion") return ; + if (protocol.value.startsWith("volcengine-jimeng-")) return ; + if (protocol.value.startsWith("volcengine-")) return ; + if (protocol.value.startsWith("newapi-channel-")) return ; + const brand = protocol.value === "gemini-veo" ? "gemini" : protocol.value === "xai-video" ? "grok" : "openai"; + return ; +} + +function BrandIconRow({ models, compact = false, className }: { models: string[]; compact?: boolean; className?: string }) { + return ( + + ); +} + +function modelBrandLabel(model: string) { + const labels: Record = { + openai: "OpenAI", + deepseek: "DeepSeek", + glm: "智谱 GLM", + gemini: "Google Gemini", + grok: "xAI Grok", + }; + return labels[model] || model; +} + function capabilityLabel(value: ChannelModel["capability"]) { return { text: "文本", image: "图片", video: "视频", audio: "音频", "": "待配置" }[value]; }