mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: 修复 Gemini API Key 和 Vertex AI 配置页面白屏问题
- GeminiApiKeySection: 添加 entries 和 excluded_models 的空值保护 - VertexAISection: 添加 entries 和 models 的空值保护 - 确保数组字段在 undefined 时不会导致组件崩溃
This commit is contained in:
@@ -3,14 +3,16 @@ import { Plus, Trash2, Key, Globe, Ban, Eye, EyeOff } from "lucide-react";
|
||||
import type { GeminiApiKeyEntry } from "@/hooks/useTauri";
|
||||
|
||||
interface GeminiApiKeySectionProps {
|
||||
entries: GeminiApiKeyEntry[];
|
||||
entries: GeminiApiKeyEntry[] | undefined;
|
||||
onChange: (entries: GeminiApiKeyEntry[]) => void;
|
||||
}
|
||||
|
||||
export function GeminiApiKeySection({
|
||||
entries,
|
||||
entries = [],
|
||||
onChange,
|
||||
}: GeminiApiKeySectionProps) {
|
||||
// 确保 entries 是数组
|
||||
const safeEntries = Array.isArray(entries) ? entries : [];
|
||||
const [showKeys, setShowKeys] = useState<Set<string>>(new Set());
|
||||
const [editingExclusions, setEditingExclusions] = useState<string | null>(
|
||||
null,
|
||||
@@ -36,33 +38,39 @@ export function GeminiApiKeySection({
|
||||
excluded_models: [],
|
||||
disabled: false,
|
||||
};
|
||||
onChange([...entries, newEntry]);
|
||||
onChange([...safeEntries, newEntry]);
|
||||
};
|
||||
|
||||
const updateEntry = (id: string, updates: Partial<GeminiApiKeyEntry>) => {
|
||||
onChange(entries.map((e) => (e.id === id ? { ...e, ...updates } : e)));
|
||||
onChange(safeEntries.map((e) => (e.id === id ? { ...e, ...updates } : e)));
|
||||
};
|
||||
|
||||
const removeEntry = (id: string) => {
|
||||
onChange(entries.filter((e) => e.id !== id));
|
||||
onChange(safeEntries.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
const addExclusion = (id: string) => {
|
||||
if (!exclusionInput.trim()) return;
|
||||
const entry = entries.find((e) => e.id === id);
|
||||
const entry = safeEntries.find((e) => e.id === id);
|
||||
if (entry) {
|
||||
const currentExcluded = Array.isArray(entry.excluded_models)
|
||||
? entry.excluded_models
|
||||
: [];
|
||||
updateEntry(id, {
|
||||
excluded_models: [...entry.excluded_models, exclusionInput.trim()],
|
||||
excluded_models: [...currentExcluded, exclusionInput.trim()],
|
||||
});
|
||||
setExclusionInput("");
|
||||
}
|
||||
};
|
||||
|
||||
const removeExclusion = (id: string, model: string) => {
|
||||
const entry = entries.find((e) => e.id === id);
|
||||
const entry = safeEntries.find((e) => e.id === id);
|
||||
if (entry) {
|
||||
const currentExcluded = Array.isArray(entry.excluded_models)
|
||||
? entry.excluded_models
|
||||
: [];
|
||||
updateEntry(id, {
|
||||
excluded_models: entry.excluded_models.filter((m) => m !== model),
|
||||
excluded_models: currentExcluded.filter((m) => m !== model),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -88,175 +96,185 @@ export function GeminiApiKeySection({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{entries.length === 0 ? (
|
||||
{safeEntries.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-6 text-center text-muted-foreground">
|
||||
<p>暂无 Gemini API Key</p>
|
||||
<p className="text-xs mt-1">点击上方"添加"按钮添加 API Key</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entries.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={`rounded-lg border p-4 space-y-3 ${
|
||||
entry.disabled ? "opacity-60 bg-muted/30" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium font-mono">
|
||||
{entry.id}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entry.disabled}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { disabled: e.target.checked })
|
||||
}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
禁用
|
||||
</label>
|
||||
<button
|
||||
onClick={() => removeEntry(entry.id)}
|
||||
className="p-1 rounded hover:bg-red-100 text-red-500"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showKeys.has(entry.id) ? "text" : "password"}
|
||||
value={entry.api_key}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { api_key: e.target.value })
|
||||
}
|
||||
placeholder="AIzaSy..."
|
||||
className="w-full px-3 py-1.5 pr-10 rounded border bg-background text-sm font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowKey(entry.id)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-muted"
|
||||
>
|
||||
{showKeys.has(entry.id) ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
<Globe className="h-3 w-3 inline mr-1" />
|
||||
Base URL (可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.base_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { base_url: e.target.value || null })
|
||||
}
|
||||
placeholder="https://generativelanguage.googleapis.com"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Proxy URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
代理 URL (可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.proxy_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { proxy_url: e.target.value || null })
|
||||
}
|
||||
placeholder="socks5://127.0.0.1:1080"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Excluded Models */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
<Ban className="h-3 w-3 inline mr-1" />
|
||||
排除模型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{entry.excluded_models.map((model) => (
|
||||
<span
|
||||
key={model}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
{model}
|
||||
<button
|
||||
onClick={() => removeExclusion(entry.id, model)}
|
||||
className="hover:text-red-900"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{editingExclusions === entry.id ? (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={exclusionInput}
|
||||
onChange={(e) => setExclusionInput(e.target.value)}
|
||||
placeholder="gemini-2.5-pro 或 *-preview"
|
||||
className="flex-1 px-2 py-1 rounded border bg-background text-sm"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
addExclusion(entry.id);
|
||||
{safeEntries.map((entry) => {
|
||||
// 确保 excluded_models 是数组
|
||||
const excludedModels = Array.isArray(entry.excluded_models)
|
||||
? entry.excluded_models
|
||||
: [];
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={`rounded-lg border p-4 space-y-3 ${
|
||||
entry.disabled ? "opacity-60 bg-muted/30" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium font-mono">
|
||||
{entry.id}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entry.disabled}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { disabled: e.target.checked })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
禁用
|
||||
</label>
|
||||
<button
|
||||
onClick={() => addExclusion(entry.id)}
|
||||
className="px-2 py-1 rounded bg-primary text-primary-foreground text-xs"
|
||||
onClick={() => removeEntry(entry.id)}
|
||||
className="p-1 rounded hover:bg-red-100 text-red-500"
|
||||
title="删除"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingExclusions(null);
|
||||
setExclusionInput("");
|
||||
}}
|
||||
className="px-2 py-1 rounded border text-xs"
|
||||
>
|
||||
完成
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditingExclusions(entry.id)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
+ 添加排除模型
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
支持通配符,如 *-preview 匹配所有预览模型
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showKeys.has(entry.id) ? "text" : "password"}
|
||||
value={entry.api_key || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { api_key: e.target.value })
|
||||
}
|
||||
placeholder="AIzaSy..."
|
||||
className="w-full px-3 py-1.5 pr-10 rounded border bg-background text-sm font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowKey(entry.id)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-muted"
|
||||
>
|
||||
{showKeys.has(entry.id) ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
<Globe className="h-3 w-3 inline mr-1" />
|
||||
Base URL (可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.base_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, {
|
||||
base_url: e.target.value || null,
|
||||
})
|
||||
}
|
||||
placeholder="https://generativelanguage.googleapis.com"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Proxy URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
代理 URL (可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.proxy_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, {
|
||||
proxy_url: e.target.value || null,
|
||||
})
|
||||
}
|
||||
placeholder="socks5://127.0.0.1:1080"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Excluded Models */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
<Ban className="h-3 w-3 inline mr-1" />
|
||||
排除模型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{excludedModels.map((model) => (
|
||||
<span
|
||||
key={model}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
{model}
|
||||
<button
|
||||
onClick={() => removeExclusion(entry.id, model)}
|
||||
className="hover:text-red-900"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{editingExclusions === entry.id ? (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={exclusionInput}
|
||||
onChange={(e) => setExclusionInput(e.target.value)}
|
||||
placeholder="gemini-2.5-pro 或 *-preview"
|
||||
className="flex-1 px-2 py-1 rounded border bg-background text-sm"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
addExclusion(entry.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => addExclusion(entry.id)}
|
||||
className="px-2 py-1 rounded bg-primary text-primary-foreground text-xs"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingExclusions(null);
|
||||
setExclusionInput("");
|
||||
}}
|
||||
className="px-2 py-1 rounded border text-xs"
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditingExclusions(entry.id)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
+ 添加排除模型
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
支持通配符,如 *-preview 匹配所有预览模型
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,11 +11,16 @@ import {
|
||||
import type { VertexApiKeyEntry, VertexModelAlias } from "@/hooks/useTauri";
|
||||
|
||||
interface VertexAISectionProps {
|
||||
entries: VertexApiKeyEntry[];
|
||||
entries: VertexApiKeyEntry[] | undefined;
|
||||
onChange: (entries: VertexApiKeyEntry[]) => void;
|
||||
}
|
||||
|
||||
export function VertexAISection({ entries, onChange }: VertexAISectionProps) {
|
||||
export function VertexAISection({
|
||||
entries = [],
|
||||
onChange,
|
||||
}: VertexAISectionProps) {
|
||||
// 确保 entries 是数组
|
||||
const safeEntries = Array.isArray(entries) ? entries : [];
|
||||
const [showKeys, setShowKeys] = useState<Set<string>>(new Set());
|
||||
const [editingAliases, setEditingAliases] = useState<string | null>(null);
|
||||
const [aliasName, setAliasName] = useState("");
|
||||
@@ -40,27 +45,28 @@ export function VertexAISection({ entries, onChange }: VertexAISectionProps) {
|
||||
proxy_url: null,
|
||||
disabled: false,
|
||||
};
|
||||
onChange([...entries, newEntry]);
|
||||
onChange([...safeEntries, newEntry]);
|
||||
};
|
||||
|
||||
const updateEntry = (id: string, updates: Partial<VertexApiKeyEntry>) => {
|
||||
onChange(entries.map((e) => (e.id === id ? { ...e, ...updates } : e)));
|
||||
onChange(safeEntries.map((e) => (e.id === id ? { ...e, ...updates } : e)));
|
||||
};
|
||||
|
||||
const removeEntry = (id: string) => {
|
||||
onChange(entries.filter((e) => e.id !== id));
|
||||
onChange(safeEntries.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
const addAlias = (id: string) => {
|
||||
if (!aliasName.trim() || !aliasAlias.trim()) return;
|
||||
const entry = entries.find((e) => e.id === id);
|
||||
const entry = safeEntries.find((e) => e.id === id);
|
||||
if (entry) {
|
||||
const newAlias: VertexModelAlias = {
|
||||
name: aliasName.trim(),
|
||||
alias: aliasAlias.trim(),
|
||||
};
|
||||
const currentModels = Array.isArray(entry.models) ? entry.models : [];
|
||||
updateEntry(id, {
|
||||
models: [...entry.models, newAlias],
|
||||
models: [...currentModels, newAlias],
|
||||
});
|
||||
setAliasName("");
|
||||
setAliasAlias("");
|
||||
@@ -68,10 +74,11 @@ export function VertexAISection({ entries, onChange }: VertexAISectionProps) {
|
||||
};
|
||||
|
||||
const removeAlias = (id: string, aliasToRemove: string) => {
|
||||
const entry = entries.find((e) => e.id === id);
|
||||
const entry = safeEntries.find((e) => e.id === id);
|
||||
if (entry) {
|
||||
const currentModels = Array.isArray(entry.models) ? entry.models : [];
|
||||
updateEntry(id, {
|
||||
models: entry.models.filter((m) => m.alias !== aliasToRemove),
|
||||
models: currentModels.filter((m) => m.alias !== aliasToRemove),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -97,186 +104,194 @@ export function VertexAISection({ entries, onChange }: VertexAISectionProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{entries.length === 0 ? (
|
||||
{safeEntries.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-6 text-center text-muted-foreground">
|
||||
<p>暂无 Vertex AI 凭证</p>
|
||||
<p className="text-xs mt-1">点击上方"添加"按钮添加凭证</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entries.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={`rounded-lg border p-4 space-y-3 ${
|
||||
entry.disabled ? "opacity-60 bg-muted/30" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium font-mono">
|
||||
{entry.id}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entry.disabled}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { disabled: e.target.checked })
|
||||
}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
禁用
|
||||
</label>
|
||||
<button
|
||||
onClick={() => removeEntry(entry.id)}
|
||||
className="p-1 rounded hover:bg-red-100 text-red-500"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showKeys.has(entry.id) ? "text" : "password"}
|
||||
value={entry.api_key}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { api_key: e.target.value })
|
||||
}
|
||||
placeholder="vk-..."
|
||||
className="w-full px-3 py-1.5 pr-10 rounded border bg-background text-sm font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowKey(entry.id)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-muted"
|
||||
>
|
||||
{showKeys.has(entry.id) ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
<Globe className="h-3 w-3 inline mr-1" />
|
||||
Base URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.base_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { base_url: e.target.value || null })
|
||||
}
|
||||
placeholder="https://example.com/api"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Proxy URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
代理 URL (可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.proxy_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { proxy_url: e.target.value || null })
|
||||
}
|
||||
placeholder="socks5://127.0.0.1:1080"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Model Aliases */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
模型别名映射
|
||||
</label>
|
||||
<div className="space-y-1 mb-2">
|
||||
{entry.models.map((model) => (
|
||||
<div
|
||||
key={model.alias}
|
||||
className="flex items-center gap-2 px-2 py-1 rounded bg-muted/50 text-sm"
|
||||
{safeEntries.map((entry) => {
|
||||
// 确保 models 是数组
|
||||
const models = Array.isArray(entry.models) ? entry.models : [];
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={`rounded-lg border p-4 space-y-3 ${
|
||||
entry.disabled ? "opacity-60 bg-muted/30" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium font-mono">
|
||||
{entry.id}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entry.disabled}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { disabled: e.target.checked })
|
||||
}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
禁用
|
||||
</label>
|
||||
<button
|
||||
onClick={() => removeEntry(entry.id)}
|
||||
className="p-1 rounded hover:bg-red-100 text-red-500"
|
||||
title="删除"
|
||||
>
|
||||
<span className="font-mono text-xs">{model.alias}</span>
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-mono text-xs text-primary">
|
||||
{model.name}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeAlias(entry.id, model.alias)}
|
||||
className="ml-auto text-red-500 hover:text-red-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{editingAliases === entry.id ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={aliasAlias}
|
||||
onChange={(e) => setAliasAlias(e.target.value)}
|
||||
placeholder="客户端别名"
|
||||
className="flex-1 px-2 py-1 rounded border bg-background text-sm"
|
||||
/>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={aliasName}
|
||||
onChange={(e) => setAliasName(e.target.value)}
|
||||
placeholder="上游模型名"
|
||||
className="flex-1 px-2 py-1 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => addAlias(entry.id)}
|
||||
className="px-2 py-1 rounded bg-primary text-primary-foreground text-xs"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingAliases(null);
|
||||
setAliasName("");
|
||||
setAliasAlias("");
|
||||
}}
|
||||
className="px-2 py-1 rounded border text-xs"
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditingAliases(entry.id)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
+ 添加模型别名
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
将客户端请求的模型名映射到上游实际模型名
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showKeys.has(entry.id) ? "text" : "password"}
|
||||
value={entry.api_key || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, { api_key: e.target.value })
|
||||
}
|
||||
placeholder="vk-..."
|
||||
className="w-full px-3 py-1.5 pr-10 rounded border bg-background text-sm font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShowKey(entry.id)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-muted"
|
||||
>
|
||||
{showKeys.has(entry.id) ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
<Globe className="h-3 w-3 inline mr-1" />
|
||||
Base URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.base_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, {
|
||||
base_url: e.target.value || null,
|
||||
})
|
||||
}
|
||||
placeholder="https://example.com/api"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Proxy URL */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
代理 URL (可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.proxy_url || ""}
|
||||
onChange={(e) =>
|
||||
updateEntry(entry.id, {
|
||||
proxy_url: e.target.value || null,
|
||||
})
|
||||
}
|
||||
placeholder="socks5://127.0.0.1:1080"
|
||||
className="w-full px-3 py-1.5 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Model Aliases */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
模型别名映射
|
||||
</label>
|
||||
<div className="space-y-1 mb-2">
|
||||
{models.map((model) => (
|
||||
<div
|
||||
key={model.alias}
|
||||
className="flex items-center gap-2 px-2 py-1 rounded bg-muted/50 text-sm"
|
||||
>
|
||||
<span className="font-mono text-xs">{model.alias}</span>
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-mono text-xs text-primary">
|
||||
{model.name}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeAlias(entry.id, model.alias)}
|
||||
className="ml-auto text-red-500 hover:text-red-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{editingAliases === entry.id ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={aliasAlias}
|
||||
onChange={(e) => setAliasAlias(e.target.value)}
|
||||
placeholder="客户端别名"
|
||||
className="flex-1 px-2 py-1 rounded border bg-background text-sm"
|
||||
/>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={aliasName}
|
||||
onChange={(e) => setAliasName(e.target.value)}
|
||||
placeholder="上游模型名"
|
||||
className="flex-1 px-2 py-1 rounded border bg-background text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => addAlias(entry.id)}
|
||||
className="px-2 py-1 rounded bg-primary text-primary-foreground text-xs"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingAliases(null);
|
||||
setAliasName("");
|
||||
setAliasAlias("");
|
||||
}}
|
||||
className="px-2 py-1 rounded border text-xs"
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditingAliases(entry.id)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
+ 添加模型别名
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
将客户端请求的模型名映射到上游实际模型名
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user