2026-04-13 11:34:23 +08:00
|
|
|
"use client";
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
|
import { CheckCircle2, Edit, KeyRound, List, Loader2, Plus, Trash2 } from "lucide-react";
|
2026-04-13 11:34:23 +08:00
|
|
|
import DashboardLayout from "@/components/layout/dashboard-layout";
|
2026-05-16 20:20:10 +08:00
|
|
|
import { Badge } from "@/components/ui/badge";
|
2026-04-13 11:34:23 +08:00
|
|
|
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";
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
interface ModelConfig {
|
2026-04-13 11:34:23 +08:00
|
|
|
id: number;
|
2026-05-16 20:20:10 +08:00
|
|
|
user_id: number;
|
2026-04-13 11:34:23 +08:00
|
|
|
name: string;
|
2026-05-16 20:20:10 +08:00
|
|
|
provider: string;
|
|
|
|
|
api_base: string | null;
|
|
|
|
|
chat_model: string;
|
|
|
|
|
embedding_model: string;
|
2026-04-13 11:34:23 +08:00
|
|
|
is_active: boolean;
|
2026-05-16 20:20:10 +08:00
|
|
|
has_api_key: boolean;
|
|
|
|
|
api_key_masked: string;
|
2026-04-13 11:34:23 +08:00
|
|
|
last_used_at: string | null;
|
|
|
|
|
created_at: string;
|
|
|
|
|
updated_at: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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;
|
|
|
|
|
};
|
2026-04-13 11:34:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
interface ModelConfigForm {
|
|
|
|
|
name: string;
|
|
|
|
|
provider: string;
|
|
|
|
|
api_key: string;
|
|
|
|
|
api_base: string;
|
|
|
|
|
chat_model: string;
|
|
|
|
|
embedding_model: string;
|
|
|
|
|
is_active: boolean;
|
2026-04-13 11:34:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-13 11:34:23 +08:00
|
|
|
export default function APIKeysPage() {
|
2026-05-16 20:20:10 +08:00
|
|
|
const [configs, setConfigs] = useState<ModelConfig[]>([]);
|
|
|
|
|
const [options, setOptions] = useState<ProviderOptionsResponse>(DEFAULT_PROVIDER_OPTIONS);
|
2026-04-13 11:34:23 +08:00
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
2026-05-16 20:20:10 +08:00
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
|
|
|
const [isProviderDialogOpen, setIsProviderDialogOpen] = useState(false);
|
|
|
|
|
const [editingConfig, setEditingConfig] = useState<ModelConfig | null>(null);
|
|
|
|
|
const [form, setForm] = useState<ModelConfigForm>({
|
|
|
|
|
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,
|
|
|
|
|
});
|
2026-04-13 11:34:23 +08:00
|
|
|
const { toast } = useToast();
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
const selectedProvider = useMemo(
|
|
|
|
|
() => options.providers.find((item) => item.provider === form.provider),
|
|
|
|
|
[form.provider, options.providers]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const fetchPageData = async () => {
|
|
|
|
|
setIsLoading(true);
|
2026-04-13 11:34:23 +08:00
|
|
|
try {
|
2026-05-16 20:20:10 +08:00
|
|
|
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,
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-04-13 11:34:23 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "错误",
|
2026-05-16 20:20:10 +08:00
|
|
|
description: "获取模型配置失败",
|
2026-04-13 11:34:23 +08:00
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-16 20:20:10 +08:00
|
|
|
fetchPageData();
|
2026-04-13 11:34:23 +08:00
|
|
|
}, []);
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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);
|
|
|
|
|
};
|
2026-04-13 11:34:23 +08:00
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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);
|
|
|
|
|
};
|
2026-04-13 11:34:23 +08:00
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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,
|
|
|
|
|
}));
|
2026-04-13 11:34:23 +08:00
|
|
|
};
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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);
|
2026-04-13 11:34:23 +08:00
|
|
|
try {
|
2026-05-16 20:20:10 +08:00
|
|
|
const payload: Record<string, unknown> = {
|
|
|
|
|
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();
|
|
|
|
|
}
|
2026-04-13 11:34:23 +08:00
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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]);
|
|
|
|
|
}
|
2026-04-13 11:34:23 +08:00
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
setIsEditorOpen(false);
|
|
|
|
|
toast({ title: "成功", description: "模型配置已保存" });
|
|
|
|
|
fetchPageData();
|
|
|
|
|
} catch (error: any) {
|
2026-04-13 11:34:23 +08:00
|
|
|
toast({
|
|
|
|
|
title: "错误",
|
2026-05-16 20:20:10 +08:00
|
|
|
description: error?.message || "保存模型配置失败",
|
2026-04-13 11:34:23 +08:00
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
2026-05-16 20:20:10 +08:00
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
2026-04-13 11:34:23 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
const toggleActive = async (config: ModelConfig) => {
|
2026-04-13 11:34:23 +08:00
|
|
|
try {
|
2026-05-16 20:20:10 +08:00
|
|
|
const updated = await api.put(`/api/model-configs/${config.id}`, {
|
|
|
|
|
is_active: !config.is_active,
|
2026-04-13 11:34:23 +08:00
|
|
|
});
|
2026-05-16 20:20:10 +08:00
|
|
|
setConfigs((current) =>
|
|
|
|
|
current.map((item) =>
|
|
|
|
|
item.id === updated.id
|
|
|
|
|
? updated
|
|
|
|
|
: updated.is_active
|
|
|
|
|
? { ...item, is_active: false }
|
|
|
|
|
: item
|
2026-04-13 11:34:23 +08:00
|
|
|
)
|
|
|
|
|
);
|
2026-05-16 20:20:10 +08:00
|
|
|
toast({ title: "成功", description: "启用状态已更新" });
|
|
|
|
|
} catch (error: any) {
|
2026-04-13 11:34:23 +08:00
|
|
|
toast({
|
|
|
|
|
title: "错误",
|
2026-05-16 20:20:10 +08:00
|
|
|
description: error?.message || "更新启用状态失败",
|
2026-04-13 11:34:23 +08:00
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
const deleteConfig = async (id: number) => {
|
2026-04-13 11:34:23 +08:00
|
|
|
try {
|
2026-05-16 20:20:10 +08:00
|
|
|
await api.delete(`/api/model-configs/${id}`);
|
|
|
|
|
setConfigs((current) => current.filter((item) => item.id !== id));
|
|
|
|
|
toast({ title: "成功", description: "模型配置已删除" });
|
|
|
|
|
} catch (error: any) {
|
2026-04-13 11:34:23 +08:00
|
|
|
toast({
|
|
|
|
|
title: "错误",
|
2026-05-16 20:20:10 +08:00
|
|
|
description: error?.message || "删除模型配置失败",
|
2026-04-13 11:34:23 +08:00
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
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") : "未使用";
|
|
|
|
|
|
2026-04-13 11:34:23 +08:00
|
|
|
return (
|
|
|
|
|
<DashboardLayout>
|
|
|
|
|
<div className="container mx-auto py-10">
|
2026-05-16 20:20:10 +08:00
|
|
|
<div className="mb-8 flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-bold">API 密钥</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<Dialog open={isProviderDialogOpen} onOpenChange={setIsProviderDialogOpen}>
|
2026-04-13 11:34:23 +08:00
|
|
|
<DialogTrigger asChild>
|
|
|
|
|
<Button variant="outline">
|
|
|
|
|
<List className="mr-2 h-4 w-4" />
|
2026-05-16 20:20:10 +08:00
|
|
|
支持模型
|
2026-04-13 11:34:23 +08:00
|
|
|
</Button>
|
|
|
|
|
</DialogTrigger>
|
2026-05-16 20:20:10 +08:00
|
|
|
<DialogContent className="max-w-2xl">
|
2026-04-13 11:34:23 +08:00
|
|
|
<DialogHeader>
|
2026-05-16 20:20:10 +08:00
|
|
|
<DialogTitle>支持模型</DialogTitle>
|
2026-04-13 11:34:23 +08:00
|
|
|
</DialogHeader>
|
2026-05-16 20:20:10 +08:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
{options.providers.map((provider) => (
|
|
|
|
|
<div key={provider.provider} className="rounded-md border p-4">
|
|
|
|
|
<div className="mb-3 flex items-center justify-between">
|
|
|
|
|
<div className="font-medium">{provider.label}</div>
|
|
|
|
|
<Badge variant={provider.requires_api_key ? "default" : "secondary"}>
|
|
|
|
|
{provider.requires_api_key ? "需要密钥" : "本地服务"}
|
|
|
|
|
</Badge>
|
2026-04-13 11:34:23 +08:00
|
|
|
</div>
|
2026-05-16 20:20:10 +08:00
|
|
|
<div className="grid gap-3 text-sm md:grid-cols-2">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mb-1 text-slate-500">语言模型</div>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
{provider.chat_models.map((model) => (
|
|
|
|
|
<Badge key={model} variant="outline">
|
|
|
|
|
{model}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
2026-04-13 11:34:23 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-16 20:20:10 +08:00
|
|
|
<div>
|
|
|
|
|
<div className="mb-1 text-slate-500">Embedding 模型</div>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
{provider.embedding_models.map((model) => (
|
|
|
|
|
<Badge key={model} variant="outline">
|
|
|
|
|
{model}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
2026-04-13 11:34:23 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-16 20:20:10 +08:00
|
|
|
))}
|
2026-04-13 11:34:23 +08:00
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
2026-05-16 20:20:10 +08:00
|
|
|
<Dialog open={isEditorOpen} onOpenChange={setIsEditorOpen}>
|
2026-04-13 11:34:23 +08:00
|
|
|
<DialogTrigger asChild>
|
2026-05-16 20:20:10 +08:00
|
|
|
<Button onClick={resetFormForCreate}>
|
2026-04-13 11:34:23 +08:00
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
2026-05-16 20:20:10 +08:00
|
|
|
创建配置
|
2026-04-13 11:34:23 +08:00
|
|
|
</Button>
|
|
|
|
|
</DialogTrigger>
|
2026-05-16 20:20:10 +08:00
|
|
|
<DialogContent className="max-w-xl">
|
2026-04-13 11:34:23 +08:00
|
|
|
<DialogHeader>
|
2026-05-16 20:20:10 +08:00
|
|
|
<DialogTitle>{editingConfig ? "编辑模型配置" : "创建模型配置"}</DialogTitle>
|
2026-04-13 11:34:23 +08:00
|
|
|
</DialogHeader>
|
2026-05-16 20:20:10 +08:00
|
|
|
|
2026-04-13 11:34:23 +08:00
|
|
|
<div className="grid gap-4 py-4">
|
|
|
|
|
<div className="grid gap-2">
|
2026-05-16 20:20:10 +08:00
|
|
|
<Label htmlFor="name">配置名称</Label>
|
2026-04-13 11:34:23 +08:00
|
|
|
<Input
|
|
|
|
|
id="name"
|
2026-05-16 20:20:10 +08:00
|
|
|
value={form.name}
|
|
|
|
|
onChange={(event) => setForm({ ...form, name: event.target.value })}
|
|
|
|
|
placeholder="例如:我的 DashScope"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<Label>服务商</Label>
|
|
|
|
|
<select
|
|
|
|
|
value={form.provider}
|
|
|
|
|
onChange={(event) => updateProvider(event.target.value)}
|
|
|
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
|
|
|
>
|
|
|
|
|
{options.providers.map((provider) => (
|
|
|
|
|
<option key={provider.provider} value={provider.provider}>
|
|
|
|
|
{provider.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<Label htmlFor="api-key">API 密钥</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="api-key"
|
|
|
|
|
type="password"
|
|
|
|
|
value={form.api_key}
|
|
|
|
|
onChange={(event) => setForm({ ...form, api_key: event.target.value })}
|
|
|
|
|
placeholder={editingConfig ? "留空则不修改" : "请输入 API 密钥"}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<Label htmlFor="api-base">API Base</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="api-base"
|
|
|
|
|
value={form.api_base}
|
|
|
|
|
onChange={(event) => setForm({ ...form, api_base: event.target.value })}
|
|
|
|
|
placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<Label htmlFor="chat-model">语言模型</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="chat-model"
|
|
|
|
|
list="chat-model-options"
|
|
|
|
|
value={form.chat_model}
|
|
|
|
|
onChange={(event) => setForm({ ...form, chat_model: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
<datalist id="chat-model-options">
|
|
|
|
|
{(selectedProvider?.chat_models || []).map((model) => (
|
|
|
|
|
<option key={model} value={model} />
|
|
|
|
|
))}
|
|
|
|
|
</datalist>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<Label htmlFor="embedding-model">Embedding 模型</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="embedding-model"
|
|
|
|
|
list="embedding-model-options"
|
|
|
|
|
value={form.embedding_model}
|
|
|
|
|
onChange={(event) =>
|
|
|
|
|
setForm({ ...form, embedding_model: event.target.value })
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<datalist id="embedding-model-options">
|
|
|
|
|
{(selectedProvider?.embedding_models || []).map((model) => (
|
|
|
|
|
<option key={model} value={model} />
|
|
|
|
|
))}
|
|
|
|
|
</datalist>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between rounded-md border px-3 py-2">
|
|
|
|
|
<Label htmlFor="active">启用为默认配置</Label>
|
|
|
|
|
<Switch
|
|
|
|
|
id="active"
|
|
|
|
|
checked={form.is_active}
|
|
|
|
|
onCheckedChange={(value) => setForm({ ...form, is_active: value })}
|
2026-04-13 11:34:23 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-16 20:20:10 +08:00
|
|
|
|
2026-04-13 11:34:23 +08:00
|
|
|
<DialogFooter>
|
2026-05-16 20:20:10 +08:00
|
|
|
<Button onClick={saveConfig} disabled={isSaving}>
|
|
|
|
|
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
|
|
|
保存
|
2026-04-13 11:34:23 +08:00
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="rounded-md border">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>名称</TableHead>
|
2026-05-16 20:20:10 +08:00
|
|
|
<TableHead>服务商</TableHead>
|
|
|
|
|
<TableHead>语言模型</TableHead>
|
|
|
|
|
<TableHead>Embedding 模型</TableHead>
|
2026-04-13 11:34:23 +08:00
|
|
|
<TableHead>API 密钥</TableHead>
|
|
|
|
|
<TableHead>状态</TableHead>
|
|
|
|
|
<TableHead>最近使用</TableHead>
|
|
|
|
|
<TableHead>操作</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
2026-05-16 20:20:10 +08:00
|
|
|
{isLoading ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={8} className="h-24 text-center text-slate-500">
|
|
|
|
|
加载中...
|
2026-04-13 11:34:23 +08:00
|
|
|
</TableCell>
|
2026-05-16 20:20:10 +08:00
|
|
|
</TableRow>
|
|
|
|
|
) : configs.length === 0 ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={8} className="h-24 text-center text-slate-500">
|
|
|
|
|
暂无模型 API 配置
|
2026-04-13 11:34:23 +08:00
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
2026-05-16 20:20:10 +08:00
|
|
|
) : (
|
|
|
|
|
configs.map((config) => (
|
|
|
|
|
<TableRow key={config.id}>
|
|
|
|
|
<TableCell className="font-medium">{config.name}</TableCell>
|
|
|
|
|
<TableCell>{providerLabel(config.provider)}</TableCell>
|
|
|
|
|
<TableCell className="font-mono text-sm">{config.chat_model}</TableCell>
|
|
|
|
|
<TableCell className="font-mono text-sm">{config.embedding_model}</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex items-center gap-2 font-mono text-sm">
|
|
|
|
|
<KeyRound className="h-4 w-4 text-slate-400" />
|
|
|
|
|
{config.api_key_masked || "本地服务"}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
checked={config.is_active}
|
|
|
|
|
onCheckedChange={() => toggleActive(config)}
|
|
|
|
|
/>
|
|
|
|
|
{config.is_active && (
|
|
|
|
|
<Badge className="gap-1">
|
|
|
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
|
|
|
默认
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>{formatDate(config.last_used_at)}</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => openEditDialog(config)}>
|
|
|
|
|
<Edit className="mr-1 h-4 w-4" />
|
|
|
|
|
编辑
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="destructive" size="sm" onClick={() => deleteConfig(config.id)}>
|
|
|
|
|
<Trash2 className="mr-1 h-4 w-4" />
|
|
|
|
|
删除
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
)}
|
2026-04-13 11:34:23 +08:00
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DashboardLayout>
|
|
|
|
|
);
|
|
|
|
|
}
|