mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat: 新增 OAuth 密钥专用编辑对话框 & 统一预设模型管理
- 新增 OAuthKeyEditDialog 组件,支持编辑 OAuth 账号的名称、备注、优先级、RPM 限制、缓存 TTL、熔断探测等配置 - OAuth 账号现在也支持自动获取模型功能,包含模型过滤规则配置 - 移除 OAuth 密钥编辑/权限按钮的 v-if 限制,统一操作入口 - 新增 preset_models.py 模块,统一管理 Kiro/Codex 等无 /v1/models 端点的预设模型 - 重构 Kiro 和 Codex 插件,改用统一的 create_preset_models_fetcher 工厂函数
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="isOpen"
|
||||
title="编辑账号"
|
||||
description="修改 OAuth 账号配置"
|
||||
:icon="SquarePen"
|
||||
size="xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
<form
|
||||
class="space-y-3"
|
||||
autocomplete="off"
|
||||
@submit.prevent="handleSave"
|
||||
>
|
||||
<!-- 基本信息:账号名称 + 备注 -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label for="name">账号名称 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
required
|
||||
placeholder="例如:主账号、备用账号"
|
||||
maxlength="100"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="note">备注</Label>
|
||||
<Input
|
||||
id="note"
|
||||
v-model="form.note"
|
||||
placeholder="可选的备注信息"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置项 -->
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<Label
|
||||
for="internal_priority"
|
||||
class="text-xs"
|
||||
>优先级</Label>
|
||||
<Input
|
||||
id="internal_priority"
|
||||
v-model.number="form.internal_priority"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="10"
|
||||
class="h-8"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
越小越优先
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="rpm_limit"
|
||||
class="text-xs"
|
||||
>RPM 限制</Label>
|
||||
<Input
|
||||
id="rpm_limit"
|
||||
:model-value="form.rpm_limit ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
placeholder="自适应"
|
||||
class="h-8"
|
||||
@update:model-value="(v) => form.rpm_limit = parseNullableNumberInput(v, { min: 1, max: 10000 })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
留空自适应
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="cache_ttl_minutes"
|
||||
class="text-xs"
|
||||
>缓存 TTL</Label>
|
||||
<Input
|
||||
id="cache_ttl_minutes"
|
||||
:model-value="form.cache_ttl_minutes ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="60"
|
||||
class="h-8"
|
||||
@update:model-value="(v) => form.cache_ttl_minutes = parseNumberInput(v, { min: 0, max: 60 }) ?? 5"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
分钟,0禁用
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="max_probe_interval_minutes"
|
||||
class="text-xs"
|
||||
>熔断探测</Label>
|
||||
<Input
|
||||
id="max_probe_interval_minutes"
|
||||
:model-value="form.max_probe_interval_minutes ?? ''"
|
||||
type="number"
|
||||
min="2"
|
||||
max="32"
|
||||
placeholder="32"
|
||||
class="h-8"
|
||||
@update:model-value="(v) => form.max_probe_interval_minutes = parseNumberInput(v, { min: 2, max: 32 }) ?? 32"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
2-32分钟
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动获取模型 -->
|
||||
<div class="space-y-3 py-2 px-3 rounded-md border border-border/60 bg-muted/30">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<Label class="text-sm font-medium">自动获取上游可用模型</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
定时更新上游模型, 配合模型映射使用
|
||||
</p>
|
||||
<p
|
||||
v-if="showAutoFetchWarning"
|
||||
class="text-xs text-amber-600 dark:text-amber-400"
|
||||
>
|
||||
已配置的模型权限将在下次获取时被覆盖
|
||||
</p>
|
||||
</div>
|
||||
<Switch v-model="form.auto_fetch_models" />
|
||||
</div>
|
||||
|
||||
<!-- 模型过滤规则(仅当开启自动获取时显示) -->
|
||||
<div
|
||||
v-if="form.auto_fetch_models"
|
||||
class="space-y-2 pt-2 border-t border-border/40"
|
||||
>
|
||||
<div>
|
||||
<Label class="text-xs">包含规则</Label>
|
||||
<Input
|
||||
v-model="form.model_include_patterns_text"
|
||||
placeholder="gpt-*, claude-*, 留空包含全部"
|
||||
class="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="text-xs">排除规则</Label>
|
||||
<Input
|
||||
v-model="form.model_exclude_patterns_text"
|
||||
placeholder="*-preview, *-beta"
|
||||
class="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
逗号分隔,支持 * ? 通配符,不区分大小写
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="saving || !canSave"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
|
||||
import { SquarePen } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput, parseNullableNumberInput } from '@/utils/form'
|
||||
import {
|
||||
updateProviderKey,
|
||||
type EndpointAPIKey,
|
||||
type EndpointAPIKeyUpdate,
|
||||
} from '@/api/endpoints'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
editingKey: EndpointAPIKey | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
saved: []
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
|
||||
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
|
||||
const showAutoFetchWarning = computed(() => {
|
||||
if (!props.editingKey) return false
|
||||
// 原本已启用,不需要警告
|
||||
if (props.editingKey.auto_fetch_models) return false
|
||||
// 现在未启用,不需要警告
|
||||
if (!form.value.auto_fetch_models) return false
|
||||
// 检查是否有已配置的模型权限
|
||||
const allowedModels = props.editingKey.allowed_models
|
||||
if (!allowedModels) return false
|
||||
if (Array.isArray(allowedModels) && allowedModels.length === 0) return false
|
||||
if (typeof allowedModels === 'object' && Object.keys(allowedModels).length === 0) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// 表单是否可以保存
|
||||
const canSave = computed(() => {
|
||||
// 必须填写名称
|
||||
if (!form.value.name.trim()) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const isOpen = computed(() => props.open)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
internal_priority: 10,
|
||||
rpm_limit: undefined as number | null | undefined,
|
||||
cache_ttl_minutes: 5,
|
||||
max_probe_interval_minutes: 32,
|
||||
note: '',
|
||||
auto_fetch_models: false,
|
||||
model_include_patterns_text: '',
|
||||
model_exclude_patterns_text: ''
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
form.value = {
|
||||
name: '',
|
||||
internal_priority: 10,
|
||||
rpm_limit: undefined,
|
||||
cache_ttl_minutes: 5,
|
||||
max_probe_interval_minutes: 32,
|
||||
note: '',
|
||||
auto_fetch_models: false,
|
||||
model_include_patterns_text: '',
|
||||
model_exclude_patterns_text: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 加载密钥数据
|
||||
function loadKeyData() {
|
||||
if (!props.editingKey) return
|
||||
form.value = {
|
||||
name: props.editingKey.name,
|
||||
internal_priority: props.editingKey.internal_priority ?? 10,
|
||||
rpm_limit: props.editingKey.rpm_limit ?? undefined,
|
||||
cache_ttl_minutes: props.editingKey.cache_ttl_minutes ?? 5,
|
||||
max_probe_interval_minutes: props.editingKey.max_probe_interval_minutes ?? 32,
|
||||
note: props.editingKey.note || '',
|
||||
auto_fetch_models: props.editingKey.auto_fetch_models ?? false,
|
||||
model_include_patterns_text: (props.editingKey.model_include_patterns || []).join(', '),
|
||||
model_exclude_patterns_text: (props.editingKey.model_exclude_patterns || []).join(', ')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 useFormDialog 统一处理对话框逻辑
|
||||
const { handleDialogUpdate, handleCancel } = useFormDialog({
|
||||
isOpen: () => props.open,
|
||||
entity: () => props.editingKey,
|
||||
isLoading: saving,
|
||||
onClose: () => emit('close'),
|
||||
loadData: loadKeyData,
|
||||
resetForm,
|
||||
})
|
||||
|
||||
// 将逗号分隔的文本解析为数组(去空、去重)
|
||||
function parsePatternText(text: string): string[] {
|
||||
if (!text.trim()) return []
|
||||
const patterns = text
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0)
|
||||
return [...new Set(patterns)]
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!props.editingKey) {
|
||||
showError('无法保存:缺少账号信息', '错误')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const updateData: EndpointAPIKeyUpdate = {
|
||||
name: form.value.name,
|
||||
internal_priority: form.value.internal_priority,
|
||||
rpm_limit: form.value.rpm_limit,
|
||||
cache_ttl_minutes: form.value.cache_ttl_minutes,
|
||||
max_probe_interval_minutes: form.value.max_probe_interval_minutes,
|
||||
note: form.value.note,
|
||||
auto_fetch_models: form.value.auto_fetch_models,
|
||||
model_include_patterns: parsePatternText(form.value.model_include_patterns_text),
|
||||
model_exclude_patterns: parsePatternText(form.value.model_exclude_patterns_text)
|
||||
}
|
||||
|
||||
await updateProviderKey(props.editingKey.id, updateData)
|
||||
success('账号已更新', '成功')
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: any) {
|
||||
const errorMessage = parseApiError(err, '保存失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -383,7 +383,6 @@
|
||||
<RefreshCw class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="key.auth_type !== 'oauth'"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@@ -393,7 +392,6 @@
|
||||
<Shield class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="key.auth_type !== 'oauth'"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@@ -870,6 +868,15 @@
|
||||
@saved="handleKeyChanged"
|
||||
/>
|
||||
|
||||
<!-- OAuth 密钥编辑对话框 -->
|
||||
<OAuthKeyEditDialog
|
||||
v-if="open"
|
||||
:open="oauthKeyEditDialogOpen"
|
||||
:editing-key="editingKey"
|
||||
@close="oauthKeyEditDialogOpen = false"
|
||||
@saved="handleKeyChanged"
|
||||
/>
|
||||
|
||||
<!-- 模型权限对话框 -->
|
||||
<KeyAllowedModelsEditDialog
|
||||
v-if="open"
|
||||
@@ -964,7 +971,8 @@ import {
|
||||
KeyAllowedModelsEditDialog,
|
||||
ModelsTab,
|
||||
BatchAssignModelsDialog,
|
||||
OAuthAccountDialog
|
||||
OAuthAccountDialog,
|
||||
OAuthKeyEditDialog
|
||||
} from '@/features/providers/components'
|
||||
import ModelMappingTab from '@/features/providers/components/provider-tabs/ModelMappingTab.vue'
|
||||
import EndpointFormDialog from '@/features/providers/components/EndpointFormDialog.vue'
|
||||
@@ -1032,6 +1040,7 @@ const endpointDialogOpen = ref(false)
|
||||
const keyFormDialogOpen = ref(false)
|
||||
const keyPermissionsDialogOpen = ref(false)
|
||||
const oauthAccountDialogOpen = ref(false)
|
||||
const oauthKeyEditDialogOpen = ref(false)
|
||||
const currentEndpoint = ref<ProviderEndpoint | null>(null)
|
||||
const editingKey = ref<EndpointAPIKey | null>(null)
|
||||
const deleteKeyConfirmOpen = ref(false)
|
||||
@@ -1097,6 +1106,7 @@ const hasBlockingDialogOpen = computed(() =>
|
||||
keyFormDialogOpen.value ||
|
||||
keyPermissionsDialogOpen.value ||
|
||||
oauthAccountDialogOpen.value ||
|
||||
oauthKeyEditDialogOpen.value ||
|
||||
deleteKeyConfirmOpen.value ||
|
||||
modelFormDialogOpen.value ||
|
||||
batchAssignDialogOpen.value ||
|
||||
@@ -1175,6 +1185,7 @@ watch(
|
||||
keyFormDialogOpen.value = false
|
||||
keyPermissionsDialogOpen.value = false
|
||||
oauthAccountDialogOpen.value = false
|
||||
oauthKeyEditDialogOpen.value = false
|
||||
deleteKeyConfirmOpen.value = false
|
||||
batchAssignDialogOpen.value = false
|
||||
antigravityQuotaDialogOpen.value = false
|
||||
@@ -1297,7 +1308,12 @@ function handleAddKeyToFirstEndpoint() {
|
||||
function handleEditKey(endpoint: ProviderEndpoint | undefined, key: EndpointAPIKey) {
|
||||
currentEndpoint.value = endpoint || null
|
||||
editingKey.value = key
|
||||
keyFormDialogOpen.value = true
|
||||
// OAuth 密钥使用专门的编辑对话框
|
||||
if (key.auth_type === 'oauth') {
|
||||
oauthKeyEditDialogOpen.value = true
|
||||
} else {
|
||||
keyFormDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyPermissions(key: EndpointAPIKey) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export { default as ProviderDetailDrawer } from './ProviderDetailDrawer.vue'
|
||||
export { default as EndpointHealthTimeline } from './EndpointHealthTimeline.vue'
|
||||
export { default as BatchAssignModelsDialog } from './BatchAssignModelsDialog.vue'
|
||||
export { default as OAuthAccountDialog } from './OAuthAccountDialog.vue'
|
||||
export { default as OAuthKeyEditDialog } from './OAuthKeyEditDialog.vue'
|
||||
|
||||
export { default as ModelsTab } from './provider-tabs/ModelsTab.vue'
|
||||
export { default as ProviderAuthDialog } from './ProviderAuthDialog.vue'
|
||||
|
||||
Reference in New Issue
Block a user