增加代码知识库;修复文档处理内容;增加API设置

This commit is contained in:
2026-05-16 20:20:10 +08:00
parent 69b49d28b2
commit 7aa3ce3294
119 changed files with 182273 additions and 793 deletions

View File

@@ -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>