mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 优化提供商管理界面交互与术语,增强 OAuth 编辑体验
- 模型映射对话框统一术语为"客户端模型/提供商模型",修复编辑时已有映射不可见的问题 - 密钥表单包含/排除规则改为双列布局 - OAuth 密钥编辑对话框增加 dirty 状态跟踪,关闭时提示未保存更改 - 调整密钥操作按钮顺序,代理节点配置移至编辑按钮前 - preset_models.py 添加 ModelsFetcherFunc 类型别名
This commit is contained in:
@@ -272,21 +272,23 @@
|
|||||||
v-if="form.auto_fetch_models"
|
v-if="form.auto_fetch_models"
|
||||||
class="space-y-2 pt-2 border-t border-border/40"
|
class="space-y-2 pt-2 border-t border-border/40"
|
||||||
>
|
>
|
||||||
<div>
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<Label class="text-xs">包含规则</Label>
|
<div>
|
||||||
<Input
|
<Label class="text-xs">包含规则</Label>
|
||||||
v-model="form.model_include_patterns_text"
|
<Input
|
||||||
placeholder="gpt-*, claude-*, 留空包含全部"
|
v-model="form.model_include_patterns_text"
|
||||||
class="h-8 text-sm"
|
placeholder="gpt-*, claude-*, 留空包含全部"
|
||||||
/>
|
class="h-8 text-sm"
|
||||||
</div>
|
/>
|
||||||
<div>
|
</div>
|
||||||
<Label class="text-xs">排除规则</Label>
|
<div>
|
||||||
<Input
|
<Label class="text-xs">排除规则</Label>
|
||||||
v-model="form.model_exclude_patterns_text"
|
<Input
|
||||||
placeholder="*-preview, *-beta"
|
v-model="form.model_exclude_patterns_text"
|
||||||
class="h-8 text-sm"
|
placeholder="*-preview, *-beta"
|
||||||
/>
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
逗号分隔,支持 * ? 通配符,不区分大小写
|
逗号分隔,支持 * ? 通配符,不区分大小写
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Dialog
|
<Dialog
|
||||||
:model-value="open"
|
:model-value="open"
|
||||||
:title="editingGroup ? '编辑模型映射' : '添加模型映射'"
|
:title="editingGroup ? '编辑模型映射' : '添加模型映射'"
|
||||||
:description="editingGroup ? '修改映射配置' : '为模型添加名称映射'"
|
:description="editingGroup ? '修改映射配置' : '将提供商模型映射到客户端模型'"
|
||||||
:icon="Tag"
|
:icon="Tag"
|
||||||
size="lg"
|
size="lg"
|
||||||
@update:model-value="$emit('update:open', $event)"
|
@update:model-value="$emit('update:open', $event)"
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- 目标模型选择 -->
|
<!-- 目标模型选择 -->
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label class="text-xs">目标模型</Label>
|
<Label class="text-xs">客户端模型</Label>
|
||||||
<Select
|
<Select
|
||||||
:model-value="formData.modelId"
|
:model-value="formData.modelId"
|
||||||
:disabled="!!editingGroup"
|
:disabled="!!editingGroup"
|
||||||
@update:model-value="handleModelChange"
|
@update:model-value="handleModelChange"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="h-9">
|
<SelectTrigger class="h-9">
|
||||||
<SelectValue placeholder="请选择目标模型" />
|
<SelectValue placeholder="请选择客户端模型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -33,20 +33,20 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
选中的映射名称将指向此模型
|
客户端请求此模型时,将路由到选中的提供商模型
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 映射名称选择面板 -->
|
<!-- 映射名称选择面板 -->
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label class="text-xs">映射名称</Label>
|
<Label class="text-xs">提供商模型</Label>
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex-1 relative">
|
<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" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索或添加自定义映射名称..."
|
placeholder="搜索或添加自定义提供商模型..."
|
||||||
class="pl-8 h-9"
|
class="pl-8 h-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<!-- 模型列表 -->
|
<!-- 模型列表 -->
|
||||||
<div class="border rounded-lg overflow-hidden">
|
<div class="border rounded-lg overflow-hidden">
|
||||||
<div class="max-h-80 overflow-y-auto">
|
<div class="min-h-60 max-h-80 overflow-y-auto">
|
||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<div
|
<div
|
||||||
v-if="loadingModels"
|
v-if="loadingModels"
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
<Plus class="w-4 h-4 text-muted-foreground" />
|
<Plus class="w-4 h-4 text-muted-foreground" />
|
||||||
<span class="text-sm font-mono">{{ searchQuery }}</span>
|
<span class="text-sm font-mono">{{ searchQuery }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-muted-foreground">添加自定义模型</span>
|
<span class="text-xs text-muted-foreground">添加自定义提供商模型</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
class="w-4 h-4 transition-transform shrink-0"
|
class="w-4 h-4 transition-transform shrink-0"
|
||||||
:class="collapsedGroups.has('custom') ? '-rotate-90' : ''"
|
:class="collapsedGroups.has('custom') ? '-rotate-90' : ''"
|
||||||
/>
|
/>
|
||||||
<span class="text-xs font-medium">自定义映射</span>
|
<span class="text-xs font-medium">自定义模型</span>
|
||||||
<span class="text-xs text-muted-foreground">({{ customNames.length }})</span>
|
<span class="text-xs text-muted-foreground">({{ customNames.length }})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
{{ searchQuery ? '无匹配结果' : '暂无可选模型' }}
|
{{ searchQuery ? '无匹配结果' : '暂无可选模型' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs mt-1">
|
<p class="text-xs mt-1">
|
||||||
输入名称后点击添加自定义映射
|
输入模型名称后点击添加自定义提供商模型
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -486,8 +486,10 @@ function initForm() {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
modelId: props.editingGroup.model.id
|
modelId: props.editingGroup.model.id
|
||||||
}
|
}
|
||||||
selectedNames.value = props.editingGroup.aliases.map(a => a.name)
|
const existingNames = props.editingGroup.aliases.map(a => a.name)
|
||||||
allCustomNames.value = []
|
selectedNames.value = [...existingNames]
|
||||||
|
// 将已有映射名称添加到自定义列表,使其在列表中可见(可取消选中来移除映射)
|
||||||
|
allCustomNames.value = [...existingNames]
|
||||||
} else {
|
} else {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
modelId: props.preselectedModelId || ''
|
modelId: props.preselectedModelId || ''
|
||||||
|
|||||||
@@ -135,21 +135,23 @@
|
|||||||
v-if="form.auto_fetch_models"
|
v-if="form.auto_fetch_models"
|
||||||
class="space-y-2 pt-2 border-t border-border/40"
|
class="space-y-2 pt-2 border-t border-border/40"
|
||||||
>
|
>
|
||||||
<div>
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<Label class="text-xs">包含规则</Label>
|
<div>
|
||||||
<Input
|
<Label class="text-xs">包含规则</Label>
|
||||||
v-model="form.model_include_patterns_text"
|
<Input
|
||||||
placeholder="gpt-*, claude-*, 留空包含全部"
|
v-model="form.model_include_patterns_text"
|
||||||
class="h-8 text-sm"
|
placeholder="gpt-*, claude-*, 留空包含全部"
|
||||||
/>
|
class="h-8 text-sm"
|
||||||
</div>
|
/>
|
||||||
<div>
|
</div>
|
||||||
<Label class="text-xs">排除规则</Label>
|
<div>
|
||||||
<Input
|
<Label class="text-xs">排除规则</Label>
|
||||||
v-model="form.model_exclude_patterns_text"
|
<Input
|
||||||
placeholder="*-preview, *-beta"
|
v-model="form.model_exclude_patterns_text"
|
||||||
class="h-8 text-sm"
|
placeholder="*-preview, *-beta"
|
||||||
/>
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
逗号分隔,支持 * ? 通配符,不区分大小写
|
逗号分隔,支持 * ? 通配符,不区分大小写
|
||||||
@@ -176,10 +178,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
|
import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
|
||||||
import { SquarePen } from 'lucide-vue-next'
|
import { SquarePen } from 'lucide-vue-next'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { useFormDialog } from '@/composables/useFormDialog'
|
import { useFormDialog } from '@/composables/useFormDialog'
|
||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import { parseNumberInput, parseNullableNumberInput } from '@/utils/form'
|
import { parseNumberInput, parseNullableNumberInput } from '@/utils/form'
|
||||||
@@ -200,6 +203,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { success, error: showError } = useToast()
|
const { success, error: showError } = useToast()
|
||||||
|
const { confirmWarning } = useConfirm()
|
||||||
|
|
||||||
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
|
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
|
||||||
const showAutoFetchWarning = computed(() => {
|
const showAutoFetchWarning = computed(() => {
|
||||||
@@ -238,6 +242,27 @@ const form = ref({
|
|||||||
model_exclude_patterns_text: ''
|
model_exclude_patterns_text: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dirty 状态跟踪:通过快照比较判断表单是否被修改
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const formSnapshot = ref('')
|
||||||
|
|
||||||
|
function takeSnapshot() {
|
||||||
|
formSnapshot.value = JSON.stringify(form.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirty = computed(() => {
|
||||||
|
if (!formSnapshot.value) return false
|
||||||
|
return JSON.stringify(form.value) !== formSnapshot.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 对话框关闭时清除快照(快照在 loadKeyData 中拍摄)
|
||||||
|
watch(isOpen, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
formSnapshot.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.value = {
|
form.value = {
|
||||||
@@ -251,6 +276,7 @@ function resetForm() {
|
|||||||
model_include_patterns_text: '',
|
model_include_patterns_text: '',
|
||||||
model_exclude_patterns_text: ''
|
model_exclude_patterns_text: ''
|
||||||
}
|
}
|
||||||
|
formSnapshot.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载密钥数据
|
// 加载密钥数据
|
||||||
@@ -267,10 +293,15 @@ function loadKeyData() {
|
|||||||
model_include_patterns_text: (props.editingKey.model_include_patterns || []).join(', '),
|
model_include_patterns_text: (props.editingKey.model_include_patterns || []).join(', '),
|
||||||
model_exclude_patterns_text: (props.editingKey.model_exclude_patterns || []).join(', ')
|
model_exclude_patterns_text: (props.editingKey.model_exclude_patterns || []).join(', ')
|
||||||
}
|
}
|
||||||
|
// 数据加载完成后拍快照,作为 dirty 判断的基准
|
||||||
|
takeSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 useFormDialog 统一处理对话框逻辑
|
// 使用 useFormDialog 统一处理对话框逻辑
|
||||||
const { handleDialogUpdate, handleCancel } = useFormDialog({
|
const {
|
||||||
|
handleDialogUpdate: _baseHandleDialogUpdate,
|
||||||
|
handleCancel: _baseHandleCancel,
|
||||||
|
} = useFormDialog({
|
||||||
isOpen: () => props.open,
|
isOpen: () => props.open,
|
||||||
entity: () => props.editingKey,
|
entity: () => props.editingKey,
|
||||||
isLoading: saving,
|
isLoading: saving,
|
||||||
@@ -279,6 +310,23 @@ const { handleDialogUpdate, handleCancel } = useFormDialog({
|
|||||||
resetForm,
|
resetForm,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 包装关闭逻辑:有未保存更改时弹出确认
|
||||||
|
async function handleDialogUpdate(value: boolean) {
|
||||||
|
if (!value && isDirty.value) {
|
||||||
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
||||||
|
if (!confirmed) return
|
||||||
|
}
|
||||||
|
_baseHandleDialogUpdate(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
if (isDirty.value) {
|
||||||
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
||||||
|
if (!confirmed) return
|
||||||
|
}
|
||||||
|
_baseHandleCancel()
|
||||||
|
}
|
||||||
|
|
||||||
// 将逗号分隔的文本解析为数组(去空、去重)
|
// 将逗号分隔的文本解析为数组(去空、去重)
|
||||||
function parsePatternText(text: string): string[] {
|
function parsePatternText(text: string): string[] {
|
||||||
if (!text.trim()) return []
|
if (!text.trim()) return []
|
||||||
|
|||||||
@@ -391,25 +391,6 @@
|
|||||||
>
|
>
|
||||||
<Shield class="w-3.5 h-3.5" />
|
<Shield class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
title="编辑密钥"
|
|
||||||
@click="handleEditKey(endpoint, key)"
|
|
||||||
>
|
|
||||||
<Edit class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="provider.provider_type === 'antigravity'"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
title="配额详情"
|
|
||||||
@click="openAntigravityQuotaDialog(key)"
|
|
||||||
>
|
|
||||||
<BarChart3 class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<!-- 代理节点配置(仅非 custom 类型显示) -->
|
<!-- 代理节点配置(仅非 custom 类型显示) -->
|
||||||
<Popover
|
<Popover
|
||||||
v-if="provider.provider_type !== 'custom'"
|
v-if="provider.provider_type !== 'custom'"
|
||||||
@@ -459,6 +440,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
title="编辑密钥"
|
||||||
|
@click="handleEditKey(endpoint, key)"
|
||||||
|
>
|
||||||
|
<Edit class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="provider.provider_type === 'antigravity'"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
title="配额详情"
|
||||||
|
@click="openAntigravityQuotaDialog(key)"
|
||||||
|
>
|
||||||
|
<BarChart3 class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -10,8 +10,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
# Fetcher 函数签名:与 UpstreamModelsFetcherRegistry 中的 _ModelsFetcher 保持一致
|
||||||
|
# (ctx, timeout_seconds) -> (models, errors, has_success, upstream_metadata)
|
||||||
|
ModelsFetcherFunc = Callable[
|
||||||
|
[Any, float],
|
||||||
|
Awaitable[tuple[list[dict], list[str], bool, dict[str, Any] | None]],
|
||||||
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 预设模型定义
|
# 预设模型定义
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -124,7 +132,7 @@ def get_preset_models(provider_type: str) -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
def create_preset_models_fetcher(
|
def create_preset_models_fetcher(
|
||||||
provider_type: str,
|
provider_type: str,
|
||||||
) -> Any:
|
) -> ModelsFetcherFunc:
|
||||||
"""创建一个返回预设模型列表的 fetcher 函数。
|
"""创建一个返回预设模型列表的 fetcher 函数。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -136,22 +144,21 @@ def create_preset_models_fetcher(
|
|||||||
models = get_preset_models(provider_type)
|
models = get_preset_models(provider_type)
|
||||||
|
|
||||||
async def fetch_preset_models(
|
async def fetch_preset_models(
|
||||||
ctx: Any,
|
_ctx: Any,
|
||||||
timeout_seconds: float, # noqa: ARG001
|
_timeout_seconds: float,
|
||||||
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
|
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
|
||||||
"""Return preset model catalog.
|
"""Return preset model catalog.
|
||||||
|
|
||||||
This provider does not expose a /v1/models endpoint, so we skip the
|
This provider does not expose a /v1/models endpoint, so we skip the
|
||||||
HTTP call entirely and return a hardcoded list.
|
HTTP call entirely and return a hardcoded list.
|
||||||
"""
|
"""
|
||||||
_ = ctx
|
|
||||||
_ = timeout_seconds
|
|
||||||
return list(models), [], True, None
|
return list(models), [], True, None
|
||||||
|
|
||||||
return fetch_preset_models
|
return fetch_preset_models
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"ModelsFetcherFunc",
|
||||||
"PRESET_MODELS",
|
"PRESET_MODELS",
|
||||||
"create_preset_models_fetcher",
|
"create_preset_models_fetcher",
|
||||||
"get_preset_models",
|
"get_preset_models",
|
||||||
|
|||||||
Reference in New Issue
Block a user