refactor: 优化端点管理表单和上游模型获取逻辑

- 端点表单布局优化,支持 provider website 作为默认 Base URL
- 模型路由始终显示 provider_model_name
- 无端点时显示添加按钮
- 上游模型获取只针对基础 API 格式,跳过 CLI 格式
This commit is contained in:
fawney19
2026-01-20 01:32:47 +08:00
parent edf8c663f1
commit 24ef24558c
5 changed files with 93 additions and 98 deletions

View File

@@ -852,9 +852,9 @@ function getPriorityModeLabel(mode: string): string {
return labels[mode] || mode return labels[mode] || mode
} }
// 判断是否存在模型映射转换 // 判断是否存在模型映射(始终显示 provider_model_name
function hasModelMapping(provider: RoutingProviderInfo): boolean { function hasModelMapping(provider: RoutingProviderInfo): boolean {
return provider.provider_model_name !== routingData.value?.global_model_name return !!provider.provider_model_name
} }
// 获取 Key 的 allowed_models 中匹配当前 GlobalModel 的所有模型名 // 获取 Key 的 allowed_models 中匹配当前 GlobalModel 的所有模型名

View File

@@ -23,9 +23,9 @@
> >
<!-- 编辑模式 --> <!-- 编辑模式 -->
<template v-if="editingEndpointId === endpoint.id"> <template v-if="editingEndpointId === endpoint.id">
<div class="space-y-2"> <div class="space-y-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm font-medium w-24 shrink-0">{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span> <span class="text-sm font-medium">{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
<div class="flex items-center gap-1 ml-auto"> <div class="flex items-center gap-1 ml-auto">
<Button <Button
variant="ghost" variant="ghost"
@@ -48,21 +48,21 @@
</Button> </Button>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-2"> <div class="flex items-end gap-3">
<div class="space-y-1"> <div class="flex-1 min-w-0 space-y-1">
<Label class="text-xs text-muted-foreground">Base URL</Label> <Label class="text-xs text-muted-foreground">Base URL</Label>
<Input <Input
v-model="editingUrl" v-model="editingUrl"
class="h-8 text-sm" class="h-9"
placeholder="https://api.example.com" :placeholder="provider?.website || 'https://api.example.com'"
@keyup.escape="cancelEdit" @keyup.escape="cancelEdit"
/> />
</div> </div>
<div class="space-y-1"> <div class="w-52 shrink-0 space-y-1">
<Label class="text-xs text-muted-foreground">自定义路径 (可选)</Label> <Label class="text-xs text-muted-foreground">自定义路径可选</Label>
<Input <Input
v-model="editingPath" v-model="editingPath"
class="h-8 text-sm" class="h-9"
:placeholder="editingDefaultPath || '留空使用默认路径'" :placeholder="editingDefaultPath || '留空使用默认路径'"
@keyup.escape="cancelEdit" @keyup.escape="cancelEdit"
/> />
@@ -71,7 +71,7 @@
<!-- 请求头规则配置 --> <!-- 请求头规则配置 -->
<Collapsible <Collapsible
v-model:open="rulesExpanded" v-model:open="rulesExpanded"
class="mt-2" class="mt-1"
> >
<CollapsibleTrigger as-child> <CollapsibleTrigger as-child>
<button <button
@@ -96,7 +96,7 @@
<div <div
v-for="(rule, index) in editingRules" v-for="(rule, index) in editingRules"
:key="index" :key="index"
class="flex items-start gap-2 p-2 rounded bg-muted/50" class="flex items-center gap-2"
> >
<!-- 操作类型选择 --> <!-- 操作类型选择 -->
<Select <Select
@@ -104,10 +104,13 @@
:model-value="rule.action" :model-value="rule.action"
@update:model-value="(v) => updateRuleAction(index, v as 'set' | 'drop' | 'rename')" @update:model-value="(v) => updateRuleAction(index, v as 'set' | 'drop' | 'rename')"
> >
<SelectTrigger class="w-24 h-7 text-xs"> <SelectTrigger class="w-24 h-8 text-xs shrink-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent :side-offset="4"> <SelectContent
position="popper"
:side-offset="4"
>
<SelectItem value="set"> <SelectItem value="set">
覆写 覆写
</SelectItem> </SelectItem>
@@ -120,89 +123,62 @@
</SelectContent> </SelectContent>
</Select> </Select>
<!-- set: key + value --> <!-- set: key = value -->
<template v-if="rule.action === 'set'"> <template v-if="rule.action === 'set'">
<div class="flex-1 space-y-1">
<Input <Input
v-model="rule.key" v-model="rule.key"
placeholder="Header 名称" placeholder="Header 名称"
class="h-7 text-xs" :class="`w-36 h-8 text-xs ${validateRuleKey(rule.key, index) ? 'border-destructive' : ''}`"
:title="validateRuleKey(rule.key, index) || ''"
/> />
<p <span class="text-muted-foreground text-xs">=</span>
v-if="validateRuleKey(rule.key, index)"
class="text-xs text-destructive"
>
{{ validateRuleKey(rule.key, index) }}
</p>
</div>
<Input <Input
v-model="rule.value" v-model="rule.value"
placeholder="Header " placeholder="Header "
class="flex-1 h-7 text-xs" class="flex-1 h-8 text-xs"
/> />
</template> </template>
<!-- drop: key only --> <!-- drop: key -->
<template v-else-if="rule.action === 'drop'"> <template v-else-if="rule.action === 'drop'">
<div class="flex-1 space-y-1">
<Input <Input
v-model="rule.key" v-model="rule.key"
placeholder="要删除的 Header 名称" placeholder="要删除的 Header 名称"
class="h-7 text-xs" :class="`flex-1 h-8 text-xs ${validateRuleKey(rule.key, index) ? 'border-destructive' : ''}`"
:title="validateRuleKey(rule.key, index) || ''"
/> />
<p
v-if="validateRuleKey(rule.key, index)"
class="text-xs text-destructive"
>
{{ validateRuleKey(rule.key, index) }}
</p>
</div>
</template> </template>
<!-- rename: from + to --> <!-- rename: from -> to -->
<template v-else-if="rule.action === 'rename'"> <template v-else-if="rule.action === 'rename'">
<div class="flex-1 space-y-1">
<Input <Input
v-model="rule.from" v-model="rule.from"
placeholder=" Header 名称" placeholder="原名称"
class="h-7 text-xs" :class="`flex-1 h-8 text-xs ${validateRenameFrom(rule.from, index) ? 'border-destructive' : ''}`"
:title="validateRenameFrom(rule.from, index) || ''"
/> />
<p <ArrowRight class="w-4 h-4 shrink-0 text-muted-foreground" />
v-if="validateRenameFrom(rule.from, index)"
class="text-xs text-destructive"
>
{{ validateRenameFrom(rule.from, index) }}
</p>
</div>
<ArrowRight class="w-4 h-4 shrink-0 text-muted-foreground mt-1.5" />
<div class="flex-1 space-y-1">
<Input <Input
v-model="rule.to" v-model="rule.to"
placeholder=" Header 名称" placeholder="新名称"
class="h-7 text-xs" :class="`flex-1 h-8 text-xs ${validateRenameTo(rule.to, index) ? 'border-destructive' : ''}`"
:title="validateRenameTo(rule.to, index) || ''"
/> />
<p
v-if="validateRenameTo(rule.to, index)"
class="text-xs text-destructive"
>
{{ validateRenameTo(rule.to, index) }}
</p>
</div>
</template> </template>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-7 w-7 shrink-0" class="h-8 w-8 shrink-0"
@click="removeRule(index)" @click="removeRule(index)"
> >
<X class="w-3 h-3" /> <X class="w-3.5 h-3.5" />
</Button> </Button>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
class="w-full h-7 text-xs" class="w-full h-8 text-xs"
@click="addRule" @click="addRule"
> >
<Plus class="w-3 h-3 mr-1" /> <Plus class="w-3 h-3 mr-1" />
@@ -273,7 +249,7 @@
> >
<Label class="text-muted-foreground">添加新端点</Label> <Label class="text-muted-foreground">添加新端点</Label>
<div class="flex items-end gap-3"> <div class="flex items-end gap-3">
<div class="w-32 shrink-0 space-y-1.5"> <div class="w-36 shrink-0 space-y-1.5">
<Label class="text-xs">API 格式</Label> <Label class="text-xs">API 格式</Label>
<Select <Select
v-model="newEndpoint.api_format" v-model="newEndpoint.api_format"
@@ -293,31 +269,22 @@
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div class="flex-1 space-y-1.5"> <div class="flex-1 min-w-0 space-y-1.5">
<Label class="text-xs">Base URL</Label> <Label class="text-xs">Base URL</Label>
<Input <Input
v-model="newEndpoint.base_url" v-model="newEndpoint.base_url"
placeholder="https://api.example.com" :placeholder="provider?.website || 'https://api.example.com'"
class="h-9" class="h-9"
/> />
</div> </div>
<div class="w-40 shrink-0 space-y-1.5"> <div class="w-52 shrink-0 space-y-1.5">
<Label class="text-xs">自定义路径</Label> <Label class="text-xs">自定义路径(可选)</Label>
<Input <Input
v-model="newEndpoint.custom_path" v-model="newEndpoint.custom_path"
:placeholder="newEndpointDefaultPath || '可选'" :placeholder="newEndpointDefaultPath || '留空使用默认路径'"
class="h-9" class="h-9"
/> />
</div> </div>
<Button
variant="outline"
size="sm"
class="h-9 shrink-0"
:disabled="!newEndpoint.api_format || !newEndpoint.base_url || addingEndpoint"
@click="handleAddEndpoint"
>
{{ addingEndpoint ? '添加中...' : '添加' }}
</Button>
</div> </div>
</div> </div>
@@ -337,6 +304,14 @@
> >
关闭 关闭
</Button> </Button>
<Button
v-if="availableFormats.length > 0"
variant="outline"
:disabled="!newEndpoint.api_format || (!newEndpoint.base_url?.trim() && !provider?.website?.trim()) || addingEndpoint"
@click="handleAddEndpoint"
>
{{ addingEndpoint ? '添加中...' : '添加' }}
</Button>
</template> </template>
</Dialog> </Dialog>
@@ -710,21 +685,27 @@ async function saveEndpointUrl(endpoint: ProviderEndpoint) {
// 添加端点 // 添加端点
async function handleAddEndpoint() { async function handleAddEndpoint() {
if (!props.provider || !newEndpoint.value.api_format || !newEndpoint.value.base_url) return if (!props.provider || !newEndpoint.value.api_format) return
// 如果没有输入 base_url使用提供商的 website 作为默认值
const baseUrl = newEndpoint.value.base_url || props.provider.website
if (!baseUrl) {
showError('请输入 Base URL')
return
}
addingEndpoint.value = true addingEndpoint.value = true
try { try {
await createEndpoint(props.provider.id, { await createEndpoint(props.provider.id, {
provider_id: props.provider.id, provider_id: props.provider.id,
api_format: newEndpoint.value.api_format, api_format: newEndpoint.value.api_format,
base_url: newEndpoint.value.base_url, base_url: baseUrl,
custom_path: newEndpoint.value.custom_path || undefined, custom_path: newEndpoint.value.custom_path || undefined,
is_active: true, is_active: true,
}) })
success(`已添加 ${API_FORMAT_LABELS[newEndpoint.value.api_format] || newEndpoint.value.api_format} 端点`) success(`已添加 ${API_FORMAT_LABELS[newEndpoint.value.api_format] || newEndpoint.value.api_format} 端点`)
// 重置表单,保留 URL // 重置表单,保留 URL
const url = newEndpoint.value.base_url newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
newEndpoint.value = { api_format: '', base_url: url, custom_path: '' }
emit('endpointCreated') emit('endpointCreated')
} catch (error: any) { } catch (error: any) {
showError(error.response?.data?.detail || '添加失败', '错误') showError(error.response?.data?.detail || '添加失败', '错误')

View File

@@ -213,9 +213,9 @@
<!-- 自动获取模型 --> <!-- 自动获取模型 -->
<div class="flex items-center justify-between py-2 px-3 rounded-md border border-border/60 bg-muted/30"> <div class="flex items-center justify-between py-2 px-3 rounded-md border border-border/60 bg-muted/30">
<div class="space-y-0.5"> <div class="space-y-0.5">
<Label class="text-sm font-medium">自动获取模型</Label> <Label class="text-sm font-medium">自动获取上游可用模型</Label>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
系统将定时从上游获取可用模型, 但无法默认接受提供商模型 定时更新上游模型, 配合模型映射使用
</p> </p>
<p <p
v-if="showAutoFetchWarning" v-if="showAutoFetchWarning"

View File

@@ -95,10 +95,21 @@
>{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span> >{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
</template> </template>
<span <span
v-if="endpoints.length > 0"
class="text-xs px-2 py-0.5 rounded-md border border-dashed border-border hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors text-muted-foreground" class="text-xs px-2 py-0.5 rounded-md border border-dashed border-border hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors text-muted-foreground"
title="编辑端点" title="编辑端点"
@click="showAddEndpointDialog" @click="showAddEndpointDialog"
>编辑</span> >编辑</span>
<Button
v-else
variant="outline"
size="sm"
class="h-7 text-xs"
@click="showAddEndpointDialog"
>
<Plus class="w-3 h-3 mr-1" />
添加 API 端点
</Button>
</div> </div>
</div> </div>

View File

@@ -20,6 +20,9 @@ from src.utils.ssl_utils import get_ssl_context
# 并发请求限制 # 并发请求限制
MAX_CONCURRENT_REQUESTS = 5 MAX_CONCURRENT_REQUESTS = 5
# 只对这些基础格式获取模型列表CLI 格式使用相同的上游 API
MODEL_FETCH_FORMATS = [APIFormat.OPENAI, APIFormat.CLAUDE, APIFormat.GEMINI]
def _get_adapter_for_format(api_format: str) -> Optional[type]: def _get_adapter_for_format(api_format: str) -> Optional[type]:
"""根据 API 格式获取对应的 Adapter 类""" """根据 API 格式获取对应的 Adapter 类"""
@@ -66,9 +69,9 @@ def build_all_format_configs(
base_url = base_endpoint.base_url base_url = base_endpoint.base_url
extra_headers = get_extra_headers_from_endpoint(base_endpoint) extra_headers = get_extra_headers_from_endpoint(base_endpoint)
# 从所有 API 格式都尝试获取模型,然后聚合去重 # 只对基础 API 格式获取模型,CLI 格式使用相同的上游 API
endpoint_configs: list[dict] = [] endpoint_configs: list[dict] = []
for fmt in APIFormat: for fmt in MODEL_FETCH_FORMATS:
fmt_value = fmt.value fmt_value = fmt.value
# 如果该格式有专门的端点配置,使用其 base_url 和 headers # 如果该格式有专门的端点配置,使用其 base_url 和 headers
if fmt_value in format_to_endpoint: if fmt_value in format_to_endpoint: