2026-01-10 18:43:53 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Dialog
|
|
|
|
|
|
:model-value="isOpen"
|
2026-01-11 19:33:16 +08:00
|
|
|
|
:title="props.apiKey?.name ? `模型权限 - ${props.apiKey.name}` : '模型权限'"
|
2026-01-14 13:12:19 +08:00
|
|
|
|
:description="isAutoFetchMode ? '自动获取模式:只允许已选择的模型,锁定的模型刷新时不会被删除' : '选中的模型将被允许访问,不选择则允许全部'"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
:icon="Shield"
|
2026-01-11 19:33:16 +08:00
|
|
|
|
size="2xl"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
@update:model-value="handleDialogUpdate"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default>
|
|
|
|
|
|
<div class="space-y-4">
|
2026-01-23 12:19:01 +08:00
|
|
|
|
<!-- 搜索栏 -->
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<div class="flex-1 relative">
|
|
|
|
|
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="searchQuery"
|
|
|
|
|
|
placeholder="搜索模型或添加自定义模型..."
|
|
|
|
|
|
class="pl-8 h-9"
|
|
|
|
|
|
/>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
</div>
|
2026-01-23 12:19:01 +08:00
|
|
|
|
<!-- 已选数量徽章 -->
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="selectedModels.length === 0 && !isAutoFetchMode"
|
|
|
|
|
|
class="h-7 px-2.5 text-xs rounded-md flex items-center bg-muted text-muted-foreground shrink-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
全部模型
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-else-if="selectedModels.length === 0 && isAutoFetchMode"
|
|
|
|
|
|
class="h-7 px-2.5 text-xs rounded-md flex items-center bg-amber-500/10 text-amber-600 dark:text-amber-400 shrink-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
未选择模型
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="h-7 px-2.5 text-xs rounded-md flex items-center bg-primary/10 text-primary shrink-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
已选 {{ selectedModels.length }} 个
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<!-- 刷新上游模型按钮 -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="upstreamModelsLoaded"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="p-2 hover:bg-muted rounded-md transition-colors shrink-0"
|
|
|
|
|
|
:disabled="fetchingUpstreamModels"
|
|
|
|
|
|
title="刷新上游模型"
|
|
|
|
|
|
@click="refreshUpstreamModels"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RefreshCw
|
|
|
|
|
|
class="w-4 h-4"
|
|
|
|
|
|
:class="{ 'animate-spin': fetchingUpstreamModels }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="!fetchingUpstreamModels"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="p-2 hover:bg-muted rounded-md transition-colors shrink-0"
|
|
|
|
|
|
title="从提供商获取模型"
|
|
|
|
|
|
@click="refreshUpstreamModels"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Zap class="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<Loader2
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="w-4 h-4 animate-spin text-muted-foreground shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
2026-01-23 12:19:01 +08:00
|
|
|
|
<!-- 模型列表 -->
|
|
|
|
|
|
<div class="border rounded-lg overflow-hidden">
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<div class="max-h-96 overflow-y-auto">
|
|
|
|
|
|
<!-- 加载中 -->
|
|
|
|
|
|
<div
|
2026-03-01 23:55:26 +08:00
|
|
|
|
v-if="loadingProviderModels"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
class="flex items-center justify-center py-12"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
2026-01-10 18:43:53 +08:00
|
|
|
|
</div>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<!-- 添加自定义模型(搜索内容不在列表中时显示) -->
|
2026-01-10 18:43:53 +08:00
|
|
|
|
<div
|
2026-01-11 02:06:48 +08:00
|
|
|
|
v-if="searchQuery && canAddAsCustom"
|
2026-01-14 13:12:19 +08:00
|
|
|
|
class="px-3 py-2 border-b bg-background sticky top-0 z-30"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div
|
2026-01-11 02:06:48 +08:00
|
|
|
|
class="flex items-center justify-between px-3 py-2 rounded-lg border border-dashed hover:border-primary hover:bg-primary/5 cursor-pointer transition-colors"
|
|
|
|
|
|
@click="addCustomModel"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<Plus class="w-4 h-4 text-muted-foreground" />
|
|
|
|
|
|
<span class="text-sm font-mono">{{ searchQuery }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">添加自定义模型</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<!-- 自定义模型 -->
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<div v-if="customModels.length > 0">
|
2026-01-11 19:33:16 +08:00
|
|
|
|
<div
|
2026-01-23 12:19:01 +08:00
|
|
|
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-20 cursor-pointer hover:bg-muted/80 transition-colors"
|
2026-01-11 19:33:16 +08:00
|
|
|
|
@click="toggleGroupCollapse('custom')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<ChevronDown
|
|
|
|
|
|
class="w-4 h-4 transition-transform shrink-0"
|
|
|
|
|
|
:class="collapsedGroups.has('custom') ? '-rotate-90' : ''"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-xs font-medium">自定义模型</span>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">({{ customModels.length }})</span>
|
|
|
|
|
|
</div>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</div>
|
2026-01-11 19:33:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-show="!collapsedGroups.has('custom')"
|
|
|
|
|
|
class="space-y-1 p-2"
|
|
|
|
|
|
>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="model in sortedCustomModels"
|
|
|
|
|
|
:key="model"
|
|
|
|
|
|
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
|
|
|
|
|
@click="toggleModel(model)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
|
|
|
|
|
|
:class="selectedModels.includes(model) ? 'bg-primary border-primary' : ''"
|
|
|
|
|
|
>
|
2026-01-12 23:28:37 +08:00
|
|
|
|
<Check
|
|
|
|
|
|
v-if="selectedModels.includes(model)"
|
|
|
|
|
|
class="w-3 h-3 text-primary-foreground"
|
|
|
|
|
|
/>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</div>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<span class="text-sm font-mono truncate flex-1">{{ model }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="selectedModels.includes(model)"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="p-1 rounded hover:bg-muted-foreground/10 transition-colors shrink-0 text-muted-foreground"
|
|
|
|
|
|
:title="isLocked(model) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'"
|
|
|
|
|
|
@click="toggleLock(model, $event)"
|
|
|
|
|
|
>
|
2026-01-15 17:03:19 +08:00
|
|
|
|
<Lock
|
|
|
|
|
|
v-if="isLocked(model)"
|
|
|
|
|
|
class="w-3.5 h-3.5"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<LockOpen
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="w-3.5 h-3.5"
|
|
|
|
|
|
/>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
</button>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<!-- 提供商模型 -->
|
2026-03-01 23:55:26 +08:00
|
|
|
|
<template v-if="filteredProviderModels.length > 0">
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<!-- 标题 sticky top -->
|
2026-01-11 19:33:16 +08:00
|
|
|
|
<div
|
2026-01-23 12:19:01 +08:00
|
|
|
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-20 cursor-pointer hover:bg-muted/80 transition-colors"
|
2026-01-11 19:33:16 +08:00
|
|
|
|
@click="toggleGroupCollapse('global')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<ChevronDown
|
|
|
|
|
|
class="w-4 h-4 transition-transform shrink-0"
|
|
|
|
|
|
:class="collapsedGroups.has('global') ? '-rotate-90' : ''"
|
|
|
|
|
|
/>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<span class="text-xs font-medium">提供商模型</span>
|
2026-03-01 23:55:26 +08:00
|
|
|
|
<span class="text-xs text-muted-foreground">({{ filteredProviderModels.length }})</span>
|
2026-01-11 19:33:16 +08:00
|
|
|
|
</div>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="text-xs text-primary hover:underline"
|
2026-03-01 23:55:26 +08:00
|
|
|
|
@click.stop="toggleAllProviderModels"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
>
|
2026-03-01 23:55:26 +08:00
|
|
|
|
{{ isAllProviderModelsSelected ? '取消全选' : '全选' }}
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<!-- 内容 -->
|
2026-01-11 19:33:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-show="!collapsedGroups.has('global')"
|
|
|
|
|
|
class="space-y-1 p-2"
|
|
|
|
|
|
>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<div
|
2026-03-01 23:55:26 +08:00
|
|
|
|
v-for="model in filteredProviderModels"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
:key="model.name"
|
|
|
|
|
|
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
|
|
|
|
|
@click="toggleModel(model.name)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
|
|
|
|
|
|
:class="selectedModels.includes(model.name) ? 'bg-primary border-primary' : ''"
|
|
|
|
|
|
>
|
2026-01-12 23:28:37 +08:00
|
|
|
|
<Check
|
|
|
|
|
|
v-if="selectedModels.includes(model.name)"
|
|
|
|
|
|
class="w-3 h-3 text-primary-foreground"
|
|
|
|
|
|
/>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</div>
|
2026-01-11 19:33:16 +08:00
|
|
|
|
<div class="flex-1 min-w-0">
|
2026-01-12 23:28:37 +08:00
|
|
|
|
<p class="text-sm font-medium truncate">
|
|
|
|
|
|
{{ model.name }}
|
|
|
|
|
|
</p>
|
2026-03-01 23:55:26 +08:00
|
|
|
|
<p
|
|
|
|
|
|
v-if="model.global_model_display_name || model.global_model_name"
|
|
|
|
|
|
class="text-xs text-muted-foreground truncate font-mono"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ model.global_model_display_name || model.global_model_name }}
|
|
|
|
|
|
</p>
|
2026-01-11 19:33:16 +08:00
|
|
|
|
</div>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<button
|
|
|
|
|
|
v-if="selectedModels.includes(model.name)"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="p-1 rounded hover:bg-muted-foreground/10 transition-colors shrink-0 text-muted-foreground"
|
|
|
|
|
|
:title="isLocked(model.name) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'"
|
|
|
|
|
|
@click="toggleLock(model.name, $event)"
|
|
|
|
|
|
>
|
2026-01-15 17:03:19 +08:00
|
|
|
|
<Lock
|
|
|
|
|
|
v-if="isLocked(model.name)"
|
|
|
|
|
|
class="w-3.5 h-3.5"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<LockOpen
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="w-3.5 h-3.5"
|
|
|
|
|
|
/>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
</button>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
</template>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<!-- 上游模型 -->
|
|
|
|
|
|
<template v-if="filteredUpstreamModels.length > 0">
|
2026-01-23 12:19:01 +08:00
|
|
|
|
<!-- 标题 sticky -->
|
2026-01-11 19:33:16 +08:00
|
|
|
|
<div
|
2026-01-23 12:19:01 +08:00
|
|
|
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-20 cursor-pointer hover:bg-muted/80 transition-colors"
|
2026-01-14 13:12:19 +08:00
|
|
|
|
@click="toggleGroupCollapse('upstream')"
|
2026-01-11 19:33:16 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<ChevronDown
|
|
|
|
|
|
class="w-4 h-4 transition-transform shrink-0"
|
2026-01-14 13:12:19 +08:00
|
|
|
|
:class="collapsedGroups.has('upstream') ? '-rotate-90' : ''"
|
2026-01-11 19:33:16 +08:00
|
|
|
|
/>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<span class="text-xs font-medium">上游模型</span>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">({{ upstreamModelNames.length }})</span>
|
2026-01-18 01:56:16 +08:00
|
|
|
|
<span
|
|
|
|
|
|
v-if="isAutoFetchMode"
|
|
|
|
|
|
class="text-xs text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
(自动同步)
|
|
|
|
|
|
</span>
|
2026-01-11 19:33:16 +08:00
|
|
|
|
</div>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<button
|
2026-01-18 01:56:16 +08:00
|
|
|
|
v-if="!isAutoFetchMode"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
type="button"
|
|
|
|
|
|
class="text-xs text-primary hover:underline"
|
2026-01-14 13:12:19 +08:00
|
|
|
|
@click.stop="toggleAllUpstreamModels"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
{{ isAllUpstreamModelsSelected ? '取消全选' : '全选' }}
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<!-- 内容 -->
|
2026-01-11 19:33:16 +08:00
|
|
|
|
<div
|
2026-01-14 13:12:19 +08:00
|
|
|
|
v-show="!collapsedGroups.has('upstream')"
|
2026-01-11 19:33:16 +08:00
|
|
|
|
class="space-y-1 p-2"
|
|
|
|
|
|
>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
<div
|
2026-01-14 13:12:19 +08:00
|
|
|
|
v-for="model in filteredUpstreamModels"
|
2026-01-19 19:41:45 +08:00
|
|
|
|
:key="model.id"
|
2026-01-18 01:56:16 +08:00
|
|
|
|
class="flex items-center gap-2 px-2 py-1.5 rounded"
|
|
|
|
|
|
:class="isAutoFetchMode ? 'opacity-80' : 'hover:bg-muted cursor-pointer'"
|
|
|
|
|
|
@click="handleUpstreamModelClick(model)"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
|
2026-01-18 01:56:16 +08:00
|
|
|
|
:class="[
|
2026-01-19 19:41:45 +08:00
|
|
|
|
selectedModels.includes(model.id) ? 'bg-primary border-primary' : '',
|
2026-01-18 01:56:16 +08:00
|
|
|
|
isAutoFetchMode ? 'opacity-50' : ''
|
|
|
|
|
|
]"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
>
|
2026-01-12 23:28:37 +08:00
|
|
|
|
<Check
|
2026-01-19 19:41:45 +08:00
|
|
|
|
v-if="selectedModels.includes(model.id)"
|
2026-01-12 23:28:37 +08:00
|
|
|
|
class="w-3 h-3 text-primary-foreground"
|
|
|
|
|
|
/>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</div>
|
2026-01-19 19:41:45 +08:00
|
|
|
|
<span class="text-sm font-mono truncate flex-1">{{ model.id }}</span>
|
|
|
|
|
|
<!-- API 格式标签 -->
|
|
|
|
|
|
<div class="flex items-center gap-1 shrink-0">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="fmt in model.api_formats"
|
|
|
|
|
|
:key="fmt"
|
|
|
|
|
|
class="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ API_FORMAT_SHORT[fmt] || fmt }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
<button
|
2026-01-19 19:41:45 +08:00
|
|
|
|
v-if="selectedModels.includes(model.id)"
|
2026-01-14 13:12:19 +08:00
|
|
|
|
type="button"
|
|
|
|
|
|
class="p-1 rounded hover:bg-muted-foreground/10 transition-colors shrink-0 text-muted-foreground"
|
2026-01-19 19:41:45 +08:00
|
|
|
|
:title="isLocked(model.id) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'"
|
|
|
|
|
|
@click="toggleLock(model.id, $event)"
|
2026-01-14 13:12:19 +08:00
|
|
|
|
>
|
2026-01-15 17:03:19 +08:00
|
|
|
|
<Lock
|
2026-01-19 19:41:45 +08:00
|
|
|
|
v-if="isLocked(model.id)"
|
2026-01-15 17:03:19 +08:00
|
|
|
|
class="w-3.5 h-3.5"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<LockOpen
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="w-3.5 h-3.5"
|
|
|
|
|
|
/>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
</button>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-14 13:12:19 +08:00
|
|
|
|
</template>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
|
<div
|
2026-01-14 13:12:19 +08:00
|
|
|
|
v-if="showEmptyState"
|
2026-01-11 02:06:48 +08:00
|
|
|
|
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Shield class="w-10 h-10 mb-2 opacity-30" />
|
2026-01-12 23:28:37 +08:00
|
|
|
|
<p class="text-sm">
|
|
|
|
|
|
{{ searchQuery ? '无匹配结果' : '暂无可选模型' }}
|
|
|
|
|
|
</p>
|
2026-01-11 02:06:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div class="flex items-center justify-between w-full">
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{ hasChanges ? '有未保存的更改' : '' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
2026-01-12 23:28:37 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
:disabled="saving || !hasChanges"
|
|
|
|
|
|
@click="handleSave"
|
|
|
|
|
|
>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
{{ saving ? '保存中...' : '保存' }}
|
|
|
|
|
|
</Button>
|
2026-01-12 23:28:37 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="handleCancel"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
|
|
|
|
|
import {
|
|
|
|
|
|
Shield,
|
|
|
|
|
|
Search,
|
|
|
|
|
|
Loader2,
|
2026-01-11 02:06:48 +08:00
|
|
|
|
Plus,
|
2026-01-11 19:33:16 +08:00
|
|
|
|
Check,
|
2026-01-14 13:12:19 +08:00
|
|
|
|
ChevronDown,
|
|
|
|
|
|
Lock,
|
2026-01-16 19:10:59 +08:00
|
|
|
|
LockOpen,
|
2026-01-23 12:19:01 +08:00
|
|
|
|
RefreshCw,
|
|
|
|
|
|
Zap
|
2026-01-10 18:43:53 +08:00
|
|
|
|
} from 'lucide-vue-next'
|
2026-01-11 02:06:48 +08:00
|
|
|
|
import { Dialog, Button, Input } from '@/components/ui'
|
2026-01-10 18:43:53 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-01-11 19:33:16 +08:00
|
|
|
|
import { useConfirm } from '@/composables/useConfirm'
|
2026-01-14 13:12:19 +08:00
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2026-01-10 18:43:53 +08:00
|
|
|
|
import {
|
2026-03-01 23:55:26 +08:00
|
|
|
|
getProviderModels,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
updateProviderKey,
|
|
|
|
|
|
type EndpointAPIKey,
|
|
|
|
|
|
type AllowedModels,
|
|
|
|
|
|
} from '@/api/endpoints'
|
2026-01-14 13:12:19 +08:00
|
|
|
|
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
2026-02-09 01:05:48 +08:00
|
|
|
|
import { API_FORMAT_SHORT, type UpstreamModel } from '@/api/endpoints/types'
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
|
|
|
|
|
interface AvailableModel {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
display_name: string
|
2026-03-01 23:55:26 +08:00
|
|
|
|
global_model_name?: string
|
|
|
|
|
|
global_model_display_name?: string
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
open: boolean
|
|
|
|
|
|
apiKey: EndpointAPIKey | null
|
|
|
|
|
|
providerId: string
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
close: []
|
|
|
|
|
|
saved: []
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const { success, error: showError } = useToast()
|
2026-01-11 19:33:16 +08:00
|
|
|
|
const { confirmWarning } = useConfirm()
|
2026-01-14 13:12:19 +08:00
|
|
|
|
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
|
|
|
|
|
const isOpen = computed(() => props.open)
|
|
|
|
|
|
const saving = ref(false)
|
2026-03-01 23:55:26 +08:00
|
|
|
|
const loadingProviderModels = ref(false)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
const fetchingUpstreamModels = ref(false)
|
|
|
|
|
|
const upstreamModelsLoaded = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 用于取消异步操作的标志
|
|
|
|
|
|
let loadingCancelled = false
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索
|
|
|
|
|
|
const searchQuery = ref('')
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 可用模型列表(该 Provider 已关联的模型)
|
|
|
|
|
|
const allProviderModels = ref<AvailableModel[]>([])
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 上游模型列表(从 API 查询获取)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
const upstreamModels = ref<UpstreamModel[]>([])
|
|
|
|
|
|
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 已选中的模型
|
|
|
|
|
|
const selectedModels = ref<string[]>([])
|
|
|
|
|
|
const initialSelectedModels = ref<string[]>([])
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 已锁定的模型
|
|
|
|
|
|
const lockedModels = ref<string[]>([])
|
|
|
|
|
|
const initialLockedModels = ref<string[]>([])
|
|
|
|
|
|
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 所有添加过的自定义模型(包括已取消勾选的,保存前不消失)
|
|
|
|
|
|
const allCustomModels = ref<string[]>([])
|
|
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 是否为自动获取模式
|
|
|
|
|
|
const isAutoFetchMode = computed(() => props.apiKey?.auto_fetch_models ?? false)
|
|
|
|
|
|
|
|
|
|
|
|
// 空状态判断
|
|
|
|
|
|
const showEmptyState = computed(() => {
|
2026-03-01 23:55:26 +08:00
|
|
|
|
return filteredProviderModels.value.length === 0 &&
|
2026-01-14 13:12:19 +08:00
|
|
|
|
filteredUpstreamModels.value.length === 0 &&
|
|
|
|
|
|
customModels.value.length === 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
|
// 折叠状态
|
|
|
|
|
|
const collapsedGroups = ref<Set<string>>(new Set())
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 是否有更改
|
|
|
|
|
|
const hasChanges = computed(() => {
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 检查选中模型是否有变化
|
2026-01-11 02:06:48 +08:00
|
|
|
|
if (selectedModels.value.length !== initialSelectedModels.value.length) return true
|
|
|
|
|
|
const sorted1 = [...selectedModels.value].sort()
|
|
|
|
|
|
const sorted2 = [...initialSelectedModels.value].sort()
|
2026-01-14 13:12:19 +08:00
|
|
|
|
if (sorted1.some((v, i) => v !== sorted2[i])) return true
|
|
|
|
|
|
|
|
|
|
|
|
// 检查锁定模型是否有变化
|
|
|
|
|
|
if (lockedModels.value.length !== initialLockedModels.value.length) return true
|
|
|
|
|
|
const sortedLocked1 = [...lockedModels.value].sort()
|
|
|
|
|
|
const sortedLocked2 = [...initialLockedModels.value].sort()
|
|
|
|
|
|
return sortedLocked1.some((v, i) => v !== sortedLocked2[i])
|
2026-01-10 18:43:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 所有已知模型的集合(提供商模型 + 上游模型)
|
2026-01-11 02:06:48 +08:00
|
|
|
|
const allKnownModels = computed(() => {
|
|
|
|
|
|
const set = new Set<string>()
|
2026-03-01 23:55:26 +08:00
|
|
|
|
allProviderModels.value.forEach(m => set.add(m.name))
|
2026-01-11 02:06:48 +08:00
|
|
|
|
upstreamModels.value.forEach(m => set.add(m.id))
|
|
|
|
|
|
return set
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 提供商模型名称集合(用于判断模型是否为“提供商模型”分组项)
|
|
|
|
|
|
const providerModelNamesSet = computed(() => {
|
|
|
|
|
|
return new Set(allProviderModels.value.map(m => m.name))
|
2026-01-14 13:12:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 判断模型是否为“提供商模型”分组项
|
|
|
|
|
|
function isProviderModel(modelId: string): boolean {
|
|
|
|
|
|
return providerModelNamesSet.value.has(modelId)
|
2026-01-14 13:12:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 22:58:37 +08:00
|
|
|
|
// 上游模型列表(后端已按 id 聚合,包含 api_formats 数组)
|
|
|
|
|
|
// 这里只做排序
|
2026-01-19 19:41:45 +08:00
|
|
|
|
const upstreamModelList = computed(() => {
|
2026-01-29 22:58:37 +08:00
|
|
|
|
return [...upstreamModels.value].sort((a, b) => a.id.localeCompare(b.id))
|
2026-01-14 13:12:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-19 19:41:45 +08:00
|
|
|
|
// 上游模型名称列表(用于计数和全选判断)
|
|
|
|
|
|
const upstreamModelNames = computed(() => upstreamModelList.value.map(m => m.id))
|
|
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 过滤后的上游模型
|
|
|
|
|
|
const filteredUpstreamModels = computed(() => {
|
2026-01-19 19:41:45 +08:00
|
|
|
|
if (!searchQuery.value.trim()) return upstreamModelList.value
|
2026-01-14 13:12:19 +08:00
|
|
|
|
const query = searchQuery.value.toLowerCase()
|
2026-01-19 19:41:45 +08:00
|
|
|
|
return upstreamModelList.value.filter(m => m.id.toLowerCase().includes(query))
|
2026-01-14 13:12:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 上游模型是否全选
|
|
|
|
|
|
const isAllUpstreamModelsSelected = computed(() => {
|
|
|
|
|
|
if (filteredUpstreamModels.value.length === 0) return false
|
2026-01-19 19:41:45 +08:00
|
|
|
|
return filteredUpstreamModels.value.every(m => selectedModels.value.includes(m.id))
|
2026-01-14 13:12:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 全选/取消全选上游模型
|
|
|
|
|
|
function toggleAllUpstreamModels() {
|
2026-01-19 19:41:45 +08:00
|
|
|
|
const allIds = filteredUpstreamModels.value.map(m => m.id)
|
2026-01-14 13:12:19 +08:00
|
|
|
|
if (isAllUpstreamModelsSelected.value) {
|
|
|
|
|
|
selectedModels.value = selectedModels.value.filter(id => !allIds.includes(id))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
allIds.forEach(id => {
|
|
|
|
|
|
if (!selectedModels.value.includes(id)) {
|
|
|
|
|
|
selectedModels.value.push(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 01:56:16 +08:00
|
|
|
|
// 处理上游模型点击(自动同步模式下禁用)
|
2026-02-09 01:05:48 +08:00
|
|
|
|
function handleUpstreamModelClick(model: UpstreamModel) {
|
2026-01-18 01:56:16 +08:00
|
|
|
|
if (!isAutoFetchMode.value) {
|
2026-01-19 19:41:45 +08:00
|
|
|
|
toggleModel(model.id)
|
2026-01-18 01:56:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 自定义模型列表(显示所有添加过的,不因取消勾选而消失)
|
|
|
|
|
|
const customModels = computed(() => {
|
|
|
|
|
|
return allCustomModels.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 排序后的自定义模型(搜索命中的排前面)
|
|
|
|
|
|
const sortedCustomModels = computed(() => {
|
|
|
|
|
|
const search = searchQuery.value.toLowerCase().trim()
|
|
|
|
|
|
if (!search) return customModels.value
|
|
|
|
|
|
|
|
|
|
|
|
const matched: string[] = []
|
|
|
|
|
|
const unmatched: string[] = []
|
|
|
|
|
|
for (const m of customModels.value) {
|
|
|
|
|
|
if (m.toLowerCase().includes(search)) {
|
|
|
|
|
|
matched.push(m)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
unmatched.push(m)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return [...matched, ...unmatched]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 判断搜索内容是否可以作为自定义模型添加
|
|
|
|
|
|
const canAddAsCustom = computed(() => {
|
|
|
|
|
|
const search = searchQuery.value.trim()
|
|
|
|
|
|
if (!search) return false
|
|
|
|
|
|
// 已经选中了就不显示
|
|
|
|
|
|
if (selectedModels.value.includes(search)) return false
|
|
|
|
|
|
// 已经在自定义模型列表中就不显示
|
|
|
|
|
|
if (allCustomModels.value.includes(search)) return false
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 精确匹配提供商模型就不显示
|
|
|
|
|
|
if (allProviderModels.value.some(m => m.name === search)) return false
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 精确匹配上游模型就不显示
|
2026-01-14 13:12:19 +08:00
|
|
|
|
if (upstreamModelNames.value.includes(search)) return false
|
2026-01-11 02:06:48 +08:00
|
|
|
|
return true
|
2026-01-10 18:43:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 搜索过滤后的提供商模型
|
|
|
|
|
|
const filteredProviderModels = computed(() => {
|
|
|
|
|
|
if (!searchQuery.value.trim()) return allProviderModels.value
|
2026-01-10 18:43:53 +08:00
|
|
|
|
const query = searchQuery.value.toLowerCase()
|
2026-03-01 23:55:26 +08:00
|
|
|
|
return allProviderModels.value.filter(m =>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
m.name.toLowerCase().includes(query) ||
|
2026-03-01 23:55:26 +08:00
|
|
|
|
m.display_name.toLowerCase().includes(query) ||
|
|
|
|
|
|
(m.global_model_name || '').toLowerCase().includes(query) ||
|
|
|
|
|
|
(m.global_model_display_name || '').toLowerCase().includes(query)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 提供商模型是否全选
|
|
|
|
|
|
const isAllProviderModelsSelected = computed(() => {
|
|
|
|
|
|
if (filteredProviderModels.value.length === 0) return false
|
|
|
|
|
|
return filteredProviderModels.value.every(m => selectedModels.value.includes(m.name))
|
2026-01-10 18:43:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 切换模型选中状态
|
|
|
|
|
|
function toggleModel(modelId: string) {
|
|
|
|
|
|
const idx = selectedModels.value.indexOf(modelId)
|
|
|
|
|
|
if (idx === -1) {
|
|
|
|
|
|
selectedModels.value.push(modelId)
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 自动获取模式下,勾选提供商模型时自动锁定
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 防止下次刷新时被覆盖(即使全局模型与上游模型同名)
|
2026-03-01 23:55:26 +08:00
|
|
|
|
if (isAutoFetchMode.value && isProviderModel(modelId)) {
|
2026-01-14 13:12:19 +08:00
|
|
|
|
if (!lockedModels.value.includes(modelId)) {
|
|
|
|
|
|
lockedModels.value.push(modelId)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-11 02:06:48 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
selectedModels.value.splice(idx, 1)
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 取消选中时也取消锁定
|
|
|
|
|
|
const lockIdx = lockedModels.value.indexOf(modelId)
|
|
|
|
|
|
if (lockIdx !== -1) {
|
|
|
|
|
|
lockedModels.value.splice(lockIdx, 1)
|
|
|
|
|
|
}
|
2026-01-11 02:06:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 切换模型锁定状态
|
|
|
|
|
|
function toggleLock(modelId: string, event: Event) {
|
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
// 只有已选中的模型才能锁定
|
|
|
|
|
|
if (!selectedModels.value.includes(modelId)) return
|
|
|
|
|
|
|
|
|
|
|
|
const idx = lockedModels.value.indexOf(modelId)
|
|
|
|
|
|
if (idx === -1) {
|
|
|
|
|
|
lockedModels.value.push(modelId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lockedModels.value.splice(idx, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查模型是否被锁定
|
|
|
|
|
|
function isLocked(modelId: string): boolean {
|
|
|
|
|
|
return lockedModels.value.includes(modelId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 添加自定义模型
|
|
|
|
|
|
function addCustomModel() {
|
|
|
|
|
|
const model = searchQuery.value.trim()
|
|
|
|
|
|
if (model && !selectedModels.value.includes(model)) {
|
|
|
|
|
|
selectedModels.value.push(model)
|
|
|
|
|
|
// 同时添加到自定义模型列表(如果不在已知模型中)
|
|
|
|
|
|
if (!allKnownModels.value.has(model) && !allCustomModels.value.includes(model)) {
|
|
|
|
|
|
allCustomModels.value.push(model)
|
|
|
|
|
|
}
|
|
|
|
|
|
searchQuery.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 全选/取消全选提供商模型
|
|
|
|
|
|
function toggleAllProviderModels() {
|
|
|
|
|
|
const allNames = filteredProviderModels.value.map(m => m.name)
|
|
|
|
|
|
if (isAllProviderModelsSelected.value) {
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 取消全选
|
|
|
|
|
|
selectedModels.value = selectedModels.value.filter(id => !allNames.includes(id))
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 同时取消锁定
|
|
|
|
|
|
lockedModels.value = lockedModels.value.filter(id => !allNames.includes(id))
|
2026-01-11 02:06:48 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 全选
|
|
|
|
|
|
allNames.forEach(name => {
|
|
|
|
|
|
if (!selectedModels.value.includes(name)) {
|
|
|
|
|
|
selectedModels.value.push(name)
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 自动获取模式下,勾选提供商模型时自动锁定
|
2026-01-14 13:12:19 +08:00
|
|
|
|
if (isAutoFetchMode.value && !lockedModels.value.includes(name)) {
|
|
|
|
|
|
lockedModels.value.push(name)
|
|
|
|
|
|
}
|
2026-01-11 02:06:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
|
// 切换折叠状态
|
|
|
|
|
|
function toggleGroupCollapse(group: string) {
|
|
|
|
|
|
if (collapsedGroups.value.has(group)) {
|
|
|
|
|
|
collapsedGroups.value.delete(group)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
collapsedGroups.value.add(group)
|
|
|
|
|
|
}
|
|
|
|
|
|
collapsedGroups.value = new Set(collapsedGroups.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
// 加载该 Provider 已关联的模型
|
|
|
|
|
|
async function loadProviderModels() {
|
|
|
|
|
|
loadingProviderModels.value = true
|
2026-01-10 18:43:53 +08:00
|
|
|
|
try {
|
2026-03-01 23:55:26 +08:00
|
|
|
|
const response = await getProviderModels(props.providerId, { limit: 1000 })
|
2026-01-10 18:43:53 +08:00
|
|
|
|
if (loadingCancelled) return
|
2026-03-01 23:55:26 +08:00
|
|
|
|
allProviderModels.value = response.map(m => ({
|
|
|
|
|
|
name: m.provider_model_name,
|
|
|
|
|
|
display_name: m.global_model_display_name || m.global_model_name || m.provider_model_name,
|
|
|
|
|
|
global_model_name: m.global_model_name,
|
|
|
|
|
|
global_model_display_name: m.global_model_display_name
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}))
|
2026-01-15 17:03:19 +08:00
|
|
|
|
} catch {
|
2026-01-10 18:43:53 +08:00
|
|
|
|
if (loadingCancelled) return
|
2026-03-01 23:55:26 +08:00
|
|
|
|
showError('加载提供商模型失败', '错误')
|
2026-01-10 18:43:53 +08:00
|
|
|
|
} finally {
|
2026-03-01 23:55:26 +08:00
|
|
|
|
loadingProviderModels.value = false
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 从提供商获取模型(使用缓存)
|
2026-02-10 11:24:22 +08:00
|
|
|
|
// 传 apiKeyId,获取当前 Key 自己能访问的上游模型
|
2026-01-16 19:10:59 +08:00
|
|
|
|
async function fetchUpstreamModels(forceRefresh = false) {
|
2026-01-10 18:43:53 +08:00
|
|
|
|
if (!props.providerId || !props.apiKey) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
fetchingUpstreamModels.value = true
|
2026-02-10 11:24:22 +08:00
|
|
|
|
const result = await fetchCachedModels(props.providerId, props.apiKey.id, forceRefresh)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
if (loadingCancelled) return
|
2026-01-14 13:12:19 +08:00
|
|
|
|
if (result.models.length > 0) {
|
|
|
|
|
|
upstreamModels.value = result.models
|
2026-01-10 18:43:53 +08:00
|
|
|
|
upstreamModelsLoaded.value = true
|
2026-01-11 02:06:48 +08:00
|
|
|
|
// 获取上游模型后,从自定义模型列表中移除已变成已知的模型
|
2026-01-14 13:12:19 +08:00
|
|
|
|
const upstreamIds = new Set(result.models.map((m: UpstreamModel) => m.id))
|
2026-01-11 02:06:48 +08:00
|
|
|
|
allCustomModels.value = allCustomModels.value.filter(m => !upstreamIds.has(m))
|
2026-01-14 13:12:19 +08:00
|
|
|
|
} else if (result.error) {
|
|
|
|
|
|
showError(result.error, '获取上游模型失败')
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
fetchingUpstreamModels.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 19:10:59 +08:00
|
|
|
|
// 手动刷新上游模型(强制跳过缓存)
|
|
|
|
|
|
async function refreshUpstreamModels() {
|
|
|
|
|
|
await fetchUpstreamModels(true)
|
|
|
|
|
|
if (upstreamModels.value.length > 0) {
|
|
|
|
|
|
success('上游模型已刷新')
|
2026-01-23 12:19:01 +08:00
|
|
|
|
// 获取成功后收缩所有分组
|
|
|
|
|
|
collapsedGroups.value = new Set(['global', 'upstream', 'custom'])
|
2026-01-16 19:10:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 解析 allowed_models
|
|
|
|
|
|
function parseAllowedModels(allowed: AllowedModels): string[] {
|
|
|
|
|
|
if (allowed === null || allowed === undefined) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
2026-01-14 17:57:21 +08:00
|
|
|
|
return [...allowed]
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
async function initializeDialogState(apiKey: EndpointAPIKey) {
|
|
|
|
|
|
loadingCancelled = false
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
const parsed = parseAllowedModels(apiKey.allowed_models ?? null)
|
|
|
|
|
|
selectedModels.value = [...parsed]
|
|
|
|
|
|
initialSelectedModels.value = [...parsed]
|
2026-01-14 13:12:19 +08:00
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
const locked = apiKey.locked_models ?? []
|
|
|
|
|
|
lockedModels.value = [...locked]
|
|
|
|
|
|
initialLockedModels.value = [...locked]
|
2026-01-14 13:12:19 +08:00
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
searchQuery.value = ''
|
|
|
|
|
|
upstreamModels.value = []
|
|
|
|
|
|
upstreamModelsLoaded.value = false
|
|
|
|
|
|
allCustomModels.value = []
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
if (apiKey.auto_fetch_models) {
|
|
|
|
|
|
collapsedGroups.value = new Set(['global', 'custom'])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
collapsedGroups.value = new Set()
|
|
|
|
|
|
}
|
2026-01-14 13:12:19 +08:00
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
await loadProviderModels()
|
2026-01-11 02:06:48 +08:00
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
if (apiKey.auto_fetch_models) {
|
|
|
|
|
|
await fetchUpstreamModels()
|
|
|
|
|
|
}
|
2026-01-14 13:12:19 +08:00
|
|
|
|
|
2026-03-07 17:11:19 +08:00
|
|
|
|
const upstreamModelIdsSet = new Set(upstreamModels.value.map(m => m.id))
|
|
|
|
|
|
allCustomModels.value = selectedModels.value.filter(m =>
|
|
|
|
|
|
!providerModelNamesSet.value.has(m) && !upstreamModelIdsSet.has(m)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听对话框打开 / 当前 key 变化
|
|
|
|
|
|
watch([() => props.open, () => props.apiKey], async ([open, apiKey]) => {
|
|
|
|
|
|
if (open && apiKey) {
|
|
|
|
|
|
await initializeDialogState(apiKey)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
loadingCancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时取消所有异步操作
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
loadingCancelled = true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
|
async function handleDialogUpdate(value: boolean) {
|
|
|
|
|
|
if (!value && hasChanges.value) {
|
|
|
|
|
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
if (!value) emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
|
async function handleCancel() {
|
|
|
|
|
|
if (hasChanges.value) {
|
|
|
|
|
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSave() {
|
|
|
|
|
|
if (!props.apiKey) return
|
|
|
|
|
|
|
|
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
2026-01-11 02:06:48 +08:00
|
|
|
|
const newAllowed: AllowedModels = selectedModels.value.length > 0
|
|
|
|
|
|
? [...selectedModels.value]
|
2026-01-10 18:43:53 +08:00
|
|
|
|
: null
|
|
|
|
|
|
|
2026-01-14 13:12:19 +08:00
|
|
|
|
// 只保存已选中且被锁定的模型
|
|
|
|
|
|
const newLocked = lockedModels.value.filter(m => selectedModels.value.includes(m))
|
|
|
|
|
|
|
|
|
|
|
|
await updateProviderKey(props.apiKey.id, {
|
|
|
|
|
|
allowed_models: newAllowed,
|
|
|
|
|
|
locked_models: newLocked
|
|
|
|
|
|
})
|
2026-01-10 18:43:53 +08:00
|
|
|
|
success('模型权限已更新', '成功')
|
|
|
|
|
|
emit('saved')
|
|
|
|
|
|
emit('close')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-01-10 18:43:53 +08:00
|
|
|
|
showError(parseApiError(err, '保存失败'), '错误')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|