import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { BotIcon, ChevronDownIcon, CircleAlertIcon, CpuIcon, EyeIcon, GripVerticalIcon, KeyRoundIcon, LoaderCircleIcon, PlusIcon, RefreshCwIcon, Settings2Icon, SparklesIcon, Trash2Icon, WrenchIcon, } from "lucide-react"; import { useMemo, useState, type ReactNode } from "react"; import { Button as TButton, Dialog, Input, InputNumber, MessagePlugin, Select, Switch, Tag, Textarea, } from "tdesign-react"; import { type ManagedModel, type ModelSavePayload, useDeleteManagedModel, useManagedModels, useReorderManagedModels, useSaveManagedModel, useTestManagedModel, } from "@/core/model-management"; import { WorkspaceBody, WorkspaceContainer, WorkspaceHeader, } from "@/components/workspace/workspace-container"; type ModelDraft = ModelSavePayload & { api_key: string; keep_existing_key: boolean; extra_settings_text: string; }; const PROVIDER_OPTIONS = [ { label: "OpenAI 兼容接口", value: "langchain_openai:ChatOpenAI" }, { label: "Anthropic", value: "langchain_anthropic:ChatAnthropic" }, { label: "Google Gemini", value: "langchain_google_genai:ChatGoogleGenerativeAI" }, { label: "Groq", value: "langchain_groq:ChatGroq" }, { label: "Ollama", value: "langchain_ollama:ChatOllama" }, ]; const emptyDraft = (): ModelDraft => ({ name: "", display_name: "", description: "", use: "langchain_openai:ChatOpenAI", model: "", api_key: "", keep_existing_key: false, clear_api_key: false, base_url: "", request_timeout: 30, max_retries: 2, max_tokens: undefined, temperature: undefined, stream_usage: true, supports_thinking: false, supports_reasoning_effort: false, supports_vision: false, when_thinking_enabled: null, when_thinking_disabled: null, thinking: null, use_responses_api: null, output_version: null, extra_settings: {}, extra_settings_text: "{}", }); function draftFromModel(model: ManagedModel): ModelDraft { return { ...model, api_key: "", keep_existing_key: model.api_key_configured, clear_api_key: false, extra_settings_text: JSON.stringify(model.extra_settings ?? {}, null, 2), }; } function optionalString(value: string | null | undefined): string | null { const normalized = (value ?? "").trim(); return normalized || null; } function toNumber(value: number | string | null | undefined): number | undefined { if (value === "" || value === null || value === undefined) return undefined; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } function buildPayload(draft: ModelDraft): ModelSavePayload { let extraSettings: Record = {}; try { const parsed = JSON.parse(draft.extra_settings_text || "{}") as unknown; if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") { throw new Error("额外参数必须是 JSON 对象"); } extraSettings = parsed as Record; } catch (error) { throw error instanceof Error ? error : new Error("额外参数不是有效 JSON"); } const payload: ModelSavePayload = { name: draft.name.trim(), display_name: optionalString(draft.display_name), description: optionalString(draft.description), use: draft.use.trim(), model: draft.model.trim(), clear_api_key: Boolean(draft.clear_api_key), base_url: optionalString(draft.base_url), request_timeout: toNumber(draft.request_timeout), max_retries: toNumber(draft.max_retries), max_tokens: toNumber(draft.max_tokens), temperature: toNumber(draft.temperature), stream_usage: draft.stream_usage, supports_thinking: draft.supports_thinking, supports_reasoning_effort: draft.supports_reasoning_effort, supports_vision: draft.supports_vision, when_thinking_enabled: draft.when_thinking_enabled, when_thinking_disabled: draft.when_thinking_disabled, thinking: draft.thinking, use_responses_api: draft.use_responses_api, output_version: optionalString(draft.output_version), extra_settings: extraSettings, }; if (draft.api_key.trim()) payload.api_key = draft.api_key.trim(); return payload; } function StatCard({ icon: Icon, label, value }: { icon: typeof CpuIcon; label: string; value: number }) { return (
{value}
{label}
); } function Field({ label, required = false, children }: { label: string; required?: boolean; children: ReactNode }) { return ; } function ModelCapabilityBadge({ icon: Icon, label, className, }: { icon?: typeof KeyRoundIcon; label: string; className: string; }) { return ( {Icon && } {label} ); } function SortableModelCard({ model, isDefault, dragEnabled, onConfigure, onDelete, }: { model: ManagedModel; isDefault: boolean; dragEnabled: boolean; onConfigure: () => void; onDelete: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: model.name, disabled: !dragEnabled, }); return (

{model.display_name || model.name}

{isDefault && 默认模型} config.yaml

{model.name}

配置 删除
{model.description ? (

{model.description}

) : (

未填写描述

)}
驱动:{model.use} 模型:{model.model}
{model.api_key_configured && ( )} {model.supports_thinking && ( )} {model.supports_vision && ( )} {model.supports_reasoning_effort && ( )}
); } function showGlobalMessage( theme: "success" | "error" | "warning", content: string, ) { // Schedule after the modal state update so the message is rendered above // the page, rather than inside or behind a closing dialog. window.setTimeout(() => { MessagePlugin.closeAll(); void MessagePlugin[theme]({ content, duration: 5_000, placement: "top", zIndex: 10_000, attach: () => document.body, }); }, 0); } function ModelEditor({ model, onClose }: { model: ManagedModel | null; onClose: () => void }) { const isNew = model === null; const [draft, setDraft] = useState(() => model ? draftFromModel(model) : emptyDraft()); const [advanced, setAdvanced] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; latency: number; message: string; preview?: string | null } | null>(null); const save = useSaveManagedModel(); const test = useTestManagedModel(); const patch = (next: Partial) => setDraft((current) => ({ ...current, ...next })); const handleTest = async () => { try { const payload = buildPayload(draft); const result = await test.mutateAsync(payload); setTestResult({ success: result.success, latency: result.latency_ms, message: result.message, preview: result.preview }); if (!result.success) return void showGlobalMessage("error", `连接失败:${result.message}`); showGlobalMessage("success", `连接成功${result.latency_ms ? `,${result.latency_ms} ms` : ""}`); } catch (error) { showGlobalMessage("error", error instanceof Error ? error.message : "连接测试失败"); } }; const handleSave = async () => { try { const payload = buildPayload(draft); if (!payload.name || !payload.use || !payload.model) { return void showGlobalMessage("warning", "请填写模型标识、驱动与实际模型名"); } await save.mutateAsync({ name: model?.name ?? payload.name, payload, isNew }); onClose(); showGlobalMessage( "success", isNew ? `模型“${payload.name}”已添加,所有页面的模型选择器已刷新。` : `模型“${payload.name}”的配置已保存并立即生效。`, ); } catch (error) { showGlobalMessage("error", error instanceof Error ? error.message : "保存模型配置失败"); } }; return ( void handleSave()} onCancel={onClose} >
保存会直接更新当前部署的 config.yaml 模型段,自动备份并热刷新;不会中断已开始的会话。
patch({ name: String(value) })} /> patch({ display_name: String(value) })} /> patch({ model: String(value) })} /> patch({ base_url: String(value) })} /> patch({ api_key: String(value), clear_api_key: false })} /> {draft.keep_existing_key && !draft.api_key &&
已配置密钥{model?.api_key_source ? `(环境变量 ${model.api_key_source})` : ""},不会回显。 patch({ clear_api_key: !draft.clear_api_key })}>{draft.clear_api_key ? "撤销清除" : "清除密钥"}
}