增加代码知识库;修复文档处理内容;增加API设置
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus, Copy, Check, List } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
@@ -28,46 +27,159 @@ import {
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export interface APIKey {
|
||||
interface ModelConfig {
|
||||
id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
key: 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;
|
||||
}
|
||||
|
||||
export interface APIKeyCreate {
|
||||
name: string;
|
||||
is_active?: boolean;
|
||||
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;
|
||||
}
|
||||
|
||||
export interface APIKeyUpdate {
|
||||
name?: string;
|
||||
is_active?: 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 [apiKeys, setApiKeys] = useState<APIKey[]>([]);
|
||||
const [configs, setConfigs] = useState<ModelConfig[]>([]);
|
||||
const [options, setOptions] = useState<ProviderOptionsResponse>(DEFAULT_PROVIDER_OPTIONS);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isAPIListDialogOpen, setIsAPIListDialogOpen] = useState(false);
|
||||
const [copiedId, setCopiedId] = useState<number | null>(null);
|
||||
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,
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
// 获取 API Keys 列表
|
||||
const fetchAPIKeys = async () => {
|
||||
const selectedProvider = useMemo(
|
||||
() => options.providers.find((item) => item.provider === form.provider),
|
||||
[form.provider, options.providers]
|
||||
);
|
||||
|
||||
const fetchPageData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await api.get("/api/api-keys");
|
||||
setApiKeys(data);
|
||||
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: "获取 API 密钥失败",
|
||||
description: "获取模型配置失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@@ -76,228 +188,304 @@ export default function APIKeysPage() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAPIKeys();
|
||||
fetchPageData();
|
||||
}, []);
|
||||
|
||||
// 创建新的 API Key
|
||||
const createAPIKey = async () => {
|
||||
if (!newKeyName.trim()) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "请输入 API 密钥名称",
|
||||
variant: "destructive",
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const data = await api.post("/api/api-keys", {
|
||||
name: newKeyName,
|
||||
is_active: true,
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
setApiKeys([...apiKeys, data]);
|
||||
setNewKeyName("");
|
||||
setIsDialogOpen(false);
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥创建成功",
|
||||
});
|
||||
} catch (error) {
|
||||
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: "创建 API 密钥失败",
|
||||
description: error?.message || "保存模型配置失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除 API Key
|
||||
const deleteAPIKey = async (id: number) => {
|
||||
const toggleActive = async (config: ModelConfig) => {
|
||||
try {
|
||||
const response = await api.delete(`/api/api-keys/${id}`);
|
||||
|
||||
if (!response.ok) throw new Error("删除 API 密钥失败");
|
||||
|
||||
setApiKeys(apiKeys.filter((key) => key.id !== id));
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥删除成功",
|
||||
const updated = await api.put(`/api/model-configs/${config.id}`, {
|
||||
is_active: !config.is_active,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "删除 API 密钥失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新 API Key 状态
|
||||
const toggleAPIKeyStatus = async (id: number, currentStatus: boolean) => {
|
||||
try {
|
||||
const response = await api.put(`/api/api-keys/${id}`, {
|
||||
is_active: !currentStatus,
|
||||
});
|
||||
|
||||
setApiKeys(
|
||||
apiKeys.map((key) =>
|
||||
key.id === id ? { ...key, is_active: !currentStatus } : key
|
||||
setConfigs((current) =>
|
||||
current.map((item) =>
|
||||
item.id === updated.id
|
||||
? updated
|
||||
: updated.is_active
|
||||
? { ...item, is_active: false }
|
||||
: item
|
||||
)
|
||||
);
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥状态更新成功",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({ title: "成功", description: "启用状态已更新" });
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "更新 API 密钥失败",
|
||||
description: error?.message || "更新启用状态失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 复制 API Key
|
||||
const copyAPIKey = async (id: number, key: string) => {
|
||||
const deleteConfig = async (id: number) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(key);
|
||||
setCopiedId(id);
|
||||
setTimeout(() => {
|
||||
setCopiedId(null);
|
||||
}, 3000);
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥已复制到剪贴板",
|
||||
});
|
||||
} catch (error) {
|
||||
await api.delete(`/api/model-configs/${id}`);
|
||||
setConfigs((current) => current.filter((item) => item.id !== id));
|
||||
toast({ title: "成功", description: "模型配置已删除" });
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "复制 API 密钥失败",
|
||||
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 (
|
||||
<DashboardLayout>
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-2xl font-bold">API 密钥</h1>
|
||||
<div className="flex gap-4">
|
||||
<Dialog
|
||||
open={isAPIListDialogOpen}
|
||||
onOpenChange={setIsAPIListDialogOpen}
|
||||
>
|
||||
<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}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<List className="mr-2 h-4 w-4" />
|
||||
API 列表
|
||||
支持模型
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>可用 API 端点</DialogTitle>
|
||||
<DialogDescription>
|
||||
查看可用 API 端点及使用方式。
|
||||
</DialogDescription>
|
||||
<DialogTitle>支持模型</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 space-y-6">
|
||||
<div className="border rounded-lg p-6 bg-slate-50">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
知识库查询
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
请求方法
|
||||
</h4>
|
||||
<code className="block p-3 bg-white border rounded-md text-sm font-mono text-blue-600">
|
||||
GET
|
||||
</code>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
接口路径
|
||||
</h4>
|
||||
<code className="block p-3 bg-white border rounded-md text-sm font-mono">
|
||||
/openapi/knowledge/{"{id}"}/query
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
查询参数
|
||||
</h4>
|
||||
<div className="bg-white border rounded-md p-3 space-y-2">
|
||||
<div className="grid grid-cols-3 text-sm">
|
||||
<div className="font-mono text-blue-600">query</div>
|
||||
<div className="col-span-2">
|
||||
查询语句
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 text-sm">
|
||||
<div className="font-mono text-blue-600">top_k</div>
|
||||
<div className="col-span-2">
|
||||
返回结果数量(可选,默认 3)
|
||||
</div>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
请求头
|
||||
</h4>
|
||||
<div className="bg-white border rounded-md p-3 grid grid-cols-3 text-sm">
|
||||
<div className="font-mono text-blue-600">
|
||||
X-API-Key
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-span-2">你的 API 密钥</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<Dialog open={isEditorOpen} onOpenChange={setIsEditorOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Button onClick={resetFormForCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
创建 API 密钥
|
||||
创建配置
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建新 API 密钥</DialogTitle>
|
||||
<DialogDescription>
|
||||
创建新的 API 密钥,用于程序化访问接口。
|
||||
</DialogDescription>
|
||||
<DialogTitle>{editingConfig ? "编辑模型配置" : "创建模型配置"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">名称</Label>
|
||||
<Label htmlFor="name">配置名称</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
placeholder="请输入 API 密钥名称"
|
||||
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 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={createAPIKey}
|
||||
disabled={isCreating || !newKeyName.trim()}
|
||||
>
|
||||
{isCreating ? "创建中..." : "创建"}
|
||||
<Button onClick={saveConfig} disabled={isSaving}>
|
||||
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -310,60 +498,71 @@ export default function APIKeysPage() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>服务商</TableHead>
|
||||
<TableHead>语言模型</TableHead>
|
||||
<TableHead>Embedding 模型</TableHead>
|
||||
<TableHead>API 密钥</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>最近使用</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((apiKey) => (
|
||||
<TableRow key={apiKey.id}>
|
||||
<TableCell>{apiKey.name}</TableCell>
|
||||
<TableCell className="flex items-center gap-2">
|
||||
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||
{apiKey.key}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => copyAPIKey(apiKey.id, apiKey.key)}
|
||||
>
|
||||
{copiedId === apiKey.id ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={apiKey.is_active}
|
||||
onCheckedChange={() =>
|
||||
toggleAPIKeyStatus(apiKey.id, apiKey.is_active)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(apiKey.created_at).toLocaleDateString("zh-CN")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{apiKey.last_used_at
|
||||
? new Date(apiKey.last_used_at).toLocaleDateString("zh-CN")
|
||||
: "从未"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteAPIKey(apiKey.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center text-slate-500">
|
||||
加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : configs.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center text-slate-500">
|
||||
暂无模型 API 配置
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
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>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user