fix(admin): persist provider settings and enforce quotas

This commit is contained in:
ZheFox
2026-08-28 12:47:54 +08:00
parent c4b4dfa996
commit dd2958a458
9 changed files with 197 additions and 68 deletions
@@ -941,7 +941,11 @@
<SelectTrigger class="h-auto w-auto gap-1.5 !border-0 bg-transparent !shadow-none p-0 font-medium rounded-none flex-row-reverse !ring-0 !ring-offset-0 !outline-none [&>svg]:h-4 [&>svg]:w-4 [&>svg]:opacity-70">
<SelectValue placeholder="选择格式..." />
</SelectTrigger>
<SelectContent>
<SelectContent
:disable-portal="false"
align="start"
class="max-h-[min(24rem,var(--radix-select-content-available-height))] min-w-[max(12rem,var(--radix-select-trigger-width))]"
>
<SelectItem
v-for="format in availableFormats"
:key="format.value"
@@ -407,7 +407,7 @@ const props = defineProps<{
const emit = defineEmits<{
close: []
saved: []
saved: [key: EndpointAPIKey]
}>()
const { success, error: showError } = useToast()
@@ -1001,11 +1001,12 @@ async function handleSave() {
updateData.auth_config = authConfig
}
await updateProviderKey(props.editingKey.id, updateData)
const updatedKey = await updateProviderKey(props.editingKey.id, updateData)
success(legacyT('密钥已更新'), legacyT('成功'))
emit('saved', updatedKey)
} else {
// 新增模式
await addProviderKey(props.providerId, {
const createdKey = await addProviderKey(props.providerId, {
api_formats: form.value.api_formats,
api_key: form.value.api_key,
auth_type: form.value.auth_type,
@@ -1027,12 +1028,11 @@ async function handleSave() {
success(legacyT('密钥已添加'), legacyT('成功'))
// 添加模式:不关闭对话框,只清除名称和密钥以便继续添加
emit('saved')
emit('saved', createdKey)
clearForNextAdd()
return
}
emit('saved')
emit('close')
} catch (err: unknown) {
const errorMessage = parseApiError(err, legacyT('保存密钥失败'))
@@ -217,7 +217,7 @@ const props = defineProps<{
const emit = defineEmits<{
close: []
saved: []
saved: [key: EndpointAPIKey]
}>()
const { success, error: showError } = useToast()
@@ -390,9 +390,9 @@ async function handleSave() {
model_exclude_patterns: parsePatternText(form.value.model_exclude_patterns_text)
}
await updateProviderKey(props.editingKey.id, updateData)
const updatedKey = await updateProviderKey(props.editingKey.id, updateData)
success('账号已更新', '成功')
emit('saved')
emit('saved', updatedKey)
emit('close')
} catch (err: unknown) {
const errorMessage = parseApiError(err, '保存失败')
@@ -2896,8 +2896,21 @@ async function openAntigravityQuotaDialog(key: EndpointAPIKey) {
}
}
async function handleKeyChanged() {
function applyUpdatedKeySnapshot(updatedKey: EndpointAPIKey) {
const index = providerKeys.value.findIndex(key => key.id === updatedKey.id)
if (index >= 0) {
providerKeys.value.splice(index, 1, updatedKey)
}
if (editingKey.value?.id === updatedKey.id) {
editingKey.value = updatedKey
}
syncCurrentSelections(endpoints.value, providerKeys.value)
}
async function handleKeyChanged(updatedKey?: EndpointAPIKey) {
if (updatedKey) applyUpdatedKeySnapshot(updatedKey)
await Promise.all([loadProvider(), loadEndpoints(), loadMappingPreview()])
if (updatedKey) applyUpdatedKeySnapshot(updatedKey)
emit('refresh')
// 添加/修改 key 后自动获取已支持 provider 的配额(新 key 的 upstream_metadata 为空)
void autoRefreshQuotaInBackground().then((changed) => {
@@ -0,0 +1,25 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
function readSource(path: string): string {
return readFileSync(resolve(process.cwd(), path), 'utf8')
}
describe('endpoint form dialog layout', () => {
it('portals the format selector outside the clipped dialog', () => {
const source = readSource('src/features/providers/components/EndpointFormDialog.vue')
const modelIndex = source.indexOf('v-model="newEndpoint.api_format"')
const selectStart = source.lastIndexOf('<Select', modelIndex)
const formatSelector = source.slice(
selectStart,
source.indexOf('</Select>', modelIndex),
)
expect(modelIndex).toBeGreaterThan(-1)
expect(selectStart).toBeGreaterThan(-1)
expect(formatSelector).toContain(':disable-portal="false"')
expect(formatSelector).toContain('var(--radix-select-content-available-height)')
expect(formatSelector).toContain('var(--radix-select-trigger-width)')
})
})
@@ -372,6 +372,9 @@ describe('provider key concurrent_limit form behavior', () => {
})
it('hydrates and serializes a positive concurrent_limit number from the normal key form', async () => {
const saved = vi.fn()
const updatedKey = createProviderKey({ rpm_limit: 42, concurrent_limit: 5 })
endpointMocks.updateProviderKey.mockResolvedValue(updatedKey)
const root = mountDialog(KeyFormDialog, {
open: true,
endpoint: null,
@@ -379,6 +382,7 @@ describe('provider key concurrent_limit form behavior', () => {
providerId: 'provider-1',
providerType: 'openai',
availableApiFormats: ['openai:chat'],
onSaved: saved,
})
await settle()
@@ -394,6 +398,7 @@ describe('provider key concurrent_limit form behavior', () => {
expect(typeof payload.concurrent_limit).toBe('number')
expect(payload.concurrent_limit).not.toBe('')
expect(payload.rpm_limit).toBe(42)
expect(saved).toHaveBeenCalledWith(updatedKey)
})
it('serializes cleared normal key concurrent_limit as null instead of an empty string', async () => {