"use client"; import { useEffect, useMemo, useState } from "react"; import { CheckCircle2, Edit, KeyRound, List, Loader2, Plus, Trash2 } from "lucide-react"; import DashboardLayout from "@/components/layout/dashboard-layout"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { useToast } from "@/components/ui/use-toast"; import { api } from "@/lib/api"; interface ModelConfig { id: number; user_id: number; name: string; provider: string; api_base: string | null; chat_model: string; embedding_model: string; is_active: boolean; has_api_key: boolean; api_key_masked: string; last_used_at: string | null; created_at: string; updated_at: string; } interface ProviderOption { provider: string; label: string; default_api_base: string; default_chat_model: string; default_embedding_model: string; chat_models: string[]; embedding_models: string[]; requires_api_key: boolean; supports_custom_api_base: boolean; } interface ProviderOptionsResponse { providers: ProviderOption[]; defaults: { provider: string; api_base: string; chat_model: string; embedding_model: string; }; } interface ModelConfigForm { name: string; provider: string; api_key: string; api_base: string; chat_model: string; embedding_model: string; is_active: boolean; } const DEFAULT_PROVIDER_OPTIONS: ProviderOptionsResponse = { providers: [ { provider: "dashscope", label: "DashScope", default_api_base: "https://dashscope.aliyuncs.com/compatible-mode/v1", default_chat_model: "qwen3-max", default_embedding_model: "text-embedding-v4", chat_models: ["qwen3-max", "qwen-plus", "qwen-turbo", "qwen-max"], embedding_models: ["text-embedding-v4", "text-embedding-v3", "text-embedding-v2"], requires_api_key: true, supports_custom_api_base: true, }, { provider: "openai", label: "OpenAI", default_api_base: "https://api.openai.com/v1", default_chat_model: "gpt-4o", default_embedding_model: "text-embedding-3-small", chat_models: ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"], embedding_models: ["text-embedding-3-small", "text-embedding-3-large", "text-embedding-ada-002"], requires_api_key: true, supports_custom_api_base: true, }, { provider: "openai_compatible", label: "OpenAI Compatible", default_api_base: "", default_chat_model: "qwen3-max", default_embedding_model: "text-embedding-v4", chat_models: ["qwen3-max", "deepseek-chat", "gpt-4o-mini"], embedding_models: ["text-embedding-v4", "text-embedding-3-small"], requires_api_key: true, supports_custom_api_base: true, }, { provider: "ollama", label: "Ollama", default_api_base: "http://localhost:11434", default_chat_model: "deepseek-r1:7b", default_embedding_model: "nomic-embed-text", chat_models: ["deepseek-r1:7b", "llama3.1", "qwen2.5"], embedding_models: ["nomic-embed-text", "mxbai-embed-large"], requires_api_key: false, supports_custom_api_base: true, }, ], defaults: { provider: "dashscope", api_base: "https://dashscope.aliyuncs.com/compatible-mode/v1", chat_model: "qwen3-max", embedding_model: "text-embedding-v4", }, }; export default function APIKeysPage() { const [configs, setConfigs] = useState([]); const [options, setOptions] = useState(DEFAULT_PROVIDER_OPTIONS); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isEditorOpen, setIsEditorOpen] = useState(false); const [isProviderDialogOpen, setIsProviderDialogOpen] = useState(false); const [editingConfig, setEditingConfig] = useState(null); const [form, setForm] = useState({ name: "", provider: DEFAULT_PROVIDER_OPTIONS.defaults.provider, api_key: "", api_base: DEFAULT_PROVIDER_OPTIONS.defaults.api_base, chat_model: DEFAULT_PROVIDER_OPTIONS.defaults.chat_model, embedding_model: DEFAULT_PROVIDER_OPTIONS.defaults.embedding_model, is_active: true, }); const { toast } = useToast(); const selectedProvider = useMemo( () => options.providers.find((item) => item.provider === form.provider), [form.provider, options.providers] ); const fetchPageData = async () => { setIsLoading(true); try { const [providerData, configData] = await Promise.all([ api.get("/api/model-configs/providers"), api.get("/api/model-configs"), ]); setOptions({ providers: providerData.providers?.length ? providerData.providers : DEFAULT_PROVIDER_OPTIONS.providers, defaults: providerData.defaults || DEFAULT_PROVIDER_OPTIONS.defaults, }); setConfigs(configData); if (!editingConfig) { setForm((current) => ({ ...current, provider: providerData.defaults?.provider || DEFAULT_PROVIDER_OPTIONS.defaults.provider, api_base: providerData.defaults?.api_base || DEFAULT_PROVIDER_OPTIONS.defaults.api_base, chat_model: providerData.defaults?.chat_model || DEFAULT_PROVIDER_OPTIONS.defaults.chat_model, embedding_model: providerData.defaults?.embedding_model || DEFAULT_PROVIDER_OPTIONS.defaults.embedding_model, })); } } catch (error) { toast({ title: "错误", description: "获取模型配置失败", variant: "destructive", }); } finally { setIsLoading(false); } }; useEffect(() => { fetchPageData(); }, []); const resetFormForCreate = () => { const defaults = options.defaults || DEFAULT_PROVIDER_OPTIONS.defaults; setEditingConfig(null); setForm({ name: "", provider: defaults.provider, api_key: "", api_base: defaults.api_base, chat_model: defaults.chat_model, embedding_model: defaults.embedding_model, is_active: true, }); setIsEditorOpen(true); }; const openEditDialog = (config: ModelConfig) => { setEditingConfig(config); setForm({ name: config.name, provider: config.provider, api_key: "", api_base: config.api_base || "", chat_model: config.chat_model, embedding_model: config.embedding_model, is_active: config.is_active, }); setIsEditorOpen(true); }; const updateProvider = (provider: string) => { const option = options.providers.find((item) => item.provider === provider); setForm((current) => ({ ...current, provider, api_base: option?.default_api_base || "", chat_model: option?.default_chat_model || current.chat_model, embedding_model: option?.default_embedding_model || current.embedding_model, })); }; const saveConfig = async () => { if (!form.name.trim()) { toast({ title: "错误", description: "请输入配置名称", variant: "destructive" }); return; } if (!editingConfig && selectedProvider?.requires_api_key && !form.api_key.trim()) { toast({ title: "错误", description: "请输入 API 密钥", variant: "destructive" }); return; } setIsSaving(true); try { const payload: Record = { name: form.name.trim(), provider: form.provider, api_base: form.api_base.trim() || null, chat_model: form.chat_model.trim(), embedding_model: form.embedding_model.trim(), is_active: form.is_active, }; if (form.api_key.trim()) { payload.api_key = form.api_key.trim(); } if (editingConfig) { const updated = await api.put(`/api/model-configs/${editingConfig.id}`, payload); setConfigs((current) => current.map((item) => (item.id === updated.id ? updated : item))); } else { const created = await api.post("/api/model-configs", { ...payload, api_key: form.api_key.trim(), }); setConfigs((current) => [created, ...current]); } setIsEditorOpen(false); toast({ title: "成功", description: "模型配置已保存" }); fetchPageData(); } catch (error: any) { toast({ title: "错误", description: error?.message || "保存模型配置失败", variant: "destructive", }); } finally { setIsSaving(false); } }; const toggleActive = async (config: ModelConfig) => { try { const updated = await api.put(`/api/model-configs/${config.id}`, { is_active: !config.is_active, }); setConfigs((current) => current.map((item) => item.id === updated.id ? updated : updated.is_active ? { ...item, is_active: false } : item ) ); toast({ title: "成功", description: "启用状态已更新" }); } catch (error: any) { toast({ title: "错误", description: error?.message || "更新启用状态失败", variant: "destructive", }); } }; const deleteConfig = async (id: number) => { try { await api.delete(`/api/model-configs/${id}`); setConfigs((current) => current.filter((item) => item.id !== id)); toast({ title: "成功", description: "模型配置已删除" }); } catch (error: any) { toast({ title: "错误", description: error?.message || "删除模型配置失败", variant: "destructive", }); } }; const providerLabel = (provider: string) => options.providers.find((item) => item.provider === provider)?.label || provider; const formatDate = (value: string | null) => value ? new Date(value).toLocaleString("zh-CN") : "未使用"; return (

API 密钥

支持模型
{options.providers.map((provider) => (
{provider.label}
{provider.requires_api_key ? "需要密钥" : "本地服务"}
语言模型
{provider.chat_models.map((model) => ( {model} ))}
Embedding 模型
{provider.embedding_models.map((model) => ( {model} ))}
))}
{editingConfig ? "编辑模型配置" : "创建模型配置"}
setForm({ ...form, name: event.target.value })} placeholder="例如:我的 DashScope" />
setForm({ ...form, api_key: event.target.value })} placeholder={editingConfig ? "留空则不修改" : "请输入 API 密钥"} />
setForm({ ...form, api_base: event.target.value })} placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1" />
setForm({ ...form, chat_model: event.target.value })} /> {(selectedProvider?.chat_models || []).map((model) => (
setForm({ ...form, embedding_model: event.target.value }) } /> {(selectedProvider?.embedding_models || []).map((model) => (
setForm({ ...form, is_active: value })} />
名称 服务商 语言模型 Embedding 模型 API 密钥 状态 最近使用 操作 {isLoading ? ( 加载中... ) : configs.length === 0 ? ( 暂无模型 API 配置 ) : ( configs.map((config) => ( {config.name} {providerLabel(config.provider)} {config.chat_model} {config.embedding_model}
{config.api_key_masked || "本地服务"}
toggleActive(config)} /> {config.is_active && ( 默认 )}
{formatDate(config.last_used_at)}
)) )}
); }