mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
Merge remote-tracking branch 'upstream/main' into codex/fix-antigravity-quota
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@
|
||||
:provider-proxy-node-name="getProviderProxyNodeName()"
|
||||
:saving-provider-proxy="savingProviderProxy"
|
||||
@toggle-format-conversion="toggleFormatConversion"
|
||||
@toggle-keep-priority-on-conversion="toggleKeepPriorityOnConversion"
|
||||
@open-failover-rules="failoverRulesDialogOpen = true"
|
||||
@set-provider-proxy="setProviderProxy"
|
||||
@clear-provider-proxy="clearProviderProxy"
|
||||
@@ -1412,6 +1413,24 @@ async function toggleFormatConversion() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleKeepPriorityOnConversion() {
|
||||
if (!provider.value) return
|
||||
const formatConversionAvailable =
|
||||
provider.value.enable_format_conversion || systemFormatConversionEnabled.value
|
||||
if (!formatConversionAvailable) return
|
||||
const newValue = !provider.value.keep_priority_on_conversion
|
||||
try {
|
||||
const updated = await updateProvider(provider.value.id, {
|
||||
keep_priority_on_conversion: newValue,
|
||||
})
|
||||
applyProviderSnapshot(updated)
|
||||
showSuccess(legacyT(newValue ? '已启用格式转换保持优先级' : '已禁用格式转换保持优先级'))
|
||||
emit('refresh')
|
||||
} catch {
|
||||
showError(legacyT('切换格式转换保持优先级失败'))
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderProxyNodeName(): string {
|
||||
const nodeId = provider.value?.proxy?.node_id
|
||||
if (!nodeId) return legacyT('未知节点')
|
||||
|
||||
@@ -24,6 +24,17 @@
|
||||
<Shuffle class="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
<span :title="keepPriorityTitle">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="provider.keep_priority_on_conversion ? 'text-primary' : ''"
|
||||
:disabled="!formatConversionAvailable"
|
||||
@click="$emit('toggleKeepPriorityOnConversion')"
|
||||
>
|
||||
<Layers class="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
<span :title="legacyT(hasFailoverRules ? '已配置故障转移规则(点击编辑)' : '配置故障转移规则')">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -163,7 +174,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Edit, GitBranch, Globe, Loader2, Plus, Power, Shuffle, X } from 'lucide-vue-next'
|
||||
import { Edit, GitBranch, Globe, Layers, Loader2, Plus, Power, Shuffle, X } from 'lucide-vue-next'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui'
|
||||
@@ -185,6 +196,7 @@ const props = defineProps<{
|
||||
|
||||
defineEmits<{
|
||||
(e: 'toggleFormatConversion'): void
|
||||
(e: 'toggleKeepPriorityOnConversion'): void
|
||||
(e: 'openFailoverRules'): void
|
||||
(e: 'update:providerProxyPopoverOpen', value: boolean): void
|
||||
(e: 'setProviderProxy', value: string): void
|
||||
@@ -203,4 +215,15 @@ const formatConversionTitle = computed(() => {
|
||||
if (props.provider.enable_format_conversion) return legacyT('已启用格式转换(点击关闭)')
|
||||
return legacyT('启用格式转换')
|
||||
})
|
||||
|
||||
const formatConversionAvailable = computed(() => (
|
||||
props.provider.enable_format_conversion || props.systemFormatConversionEnabled
|
||||
))
|
||||
|
||||
const keepPriorityTitle = computed(() => {
|
||||
if (!formatConversionAvailable.value) return legacyT('请先启用格式转换')
|
||||
return props.provider.keep_priority_on_conversion
|
||||
? legacyT('已启用格式转换保持优先级(点击关闭)')
|
||||
: legacyT('启用格式转换保持优先级')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -123,39 +123,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 计费与限流 / 请求配置 -->
|
||||
<!-- 请求配置 -->
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
{{ legacyT('计费与限流') }}
|
||||
</h3>
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
{{ legacyT('请求配置') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label>{{ legacyT('计费类型') }}</Label>
|
||||
<Select
|
||||
v-model="form.billing_type"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly_quota">
|
||||
{{ legacyT('月卡额度') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="pay_as_you_go">
|
||||
{{ legacyT('按量付费') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="free_tier">
|
||||
{{ legacyT('免费套餐') }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
{{ legacyT('请求配置') }}
|
||||
</h3>
|
||||
|
||||
<!-- 超时配置 -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
@@ -229,49 +201,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 月卡配置 -->
|
||||
<div
|
||||
v-if="form.billing_type === 'monthly_quota'"
|
||||
class="grid grid-cols-2 gap-4 p-3 border rounded-lg bg-muted/50"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">{{ legacyT('周期额度 (USD)') }}</Label>
|
||||
<Input
|
||||
:model-value="form.monthly_quota_usd ?? ''"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
@update:model-value="(v) => form.monthly_quota_usd = parseNumberInput(v, { allowFloat: true })"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">{{ legacyT('重置周期 (天)') }}</Label>
|
||||
<Input
|
||||
:model-value="form.quota_reset_day ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="365"
|
||||
@update:model-value="(v) => form.quota_reset_day = parseNumberInput(v) ?? 30"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">
|
||||
{{ legacyT('周期开始时间') }} <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
v-model="form.quota_last_reset_at"
|
||||
type="datetime-local"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">{{ legacyT('过期时间') }}</Label>
|
||||
<Input
|
||||
v-model="form.quota_expires_at"
|
||||
type="datetime-local"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能开关 -->
|
||||
@@ -280,19 +209,6 @@
|
||||
{{ legacyT('功能开关') }}
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">{{ legacyT('格式转换保持优先级') }}</span>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ legacyT('跨格式请求时保持原优先级排名,不降级到格式匹配的提供商之后') }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="form.keep_priority_on_conversion"
|
||||
@update:model-value="(v: boolean) => form.keep_priority_on_conversion = v"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">{{ legacyT('号池调度模式') }}</span>
|
||||
@@ -426,7 +342,6 @@ import {
|
||||
} from '@/api/endpoints'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import { dateTimeLocalToRfc3339, formatDateTimeLocalInput } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -468,14 +383,7 @@ const form = ref({
|
||||
provider_type: 'custom' as ProviderType,
|
||||
description: '',
|
||||
website: '',
|
||||
// 计费配置
|
||||
billing_type: 'pay_as_you_go' as 'monthly_quota' | 'pay_as_you_go' | 'free_tier',
|
||||
monthly_quota_usd: undefined as number | undefined,
|
||||
quota_reset_day: 30,
|
||||
quota_last_reset_at: '', // 周期开始时间
|
||||
quota_expires_at: '',
|
||||
provider_priority: 100,
|
||||
keep_priority_on_conversion: false, // 格式转换时是否保持优先级
|
||||
// 状态配置
|
||||
is_active: true,
|
||||
rate_limit: undefined as number | undefined,
|
||||
@@ -504,13 +412,7 @@ function resetForm() {
|
||||
provider_type: 'custom',
|
||||
description: '',
|
||||
website: '',
|
||||
billing_type: 'pay_as_you_go',
|
||||
monthly_quota_usd: undefined,
|
||||
quota_reset_day: 30,
|
||||
quota_last_reset_at: '',
|
||||
quota_expires_at: '',
|
||||
provider_priority: defaultPriority.value,
|
||||
keep_priority_on_conversion: false,
|
||||
is_active: true,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: undefined,
|
||||
@@ -542,13 +444,7 @@ function loadProviderData() {
|
||||
provider_type: props.provider.provider_type || 'custom',
|
||||
description: props.provider.description || '',
|
||||
website: props.provider.website || '',
|
||||
billing_type: (props.provider.billing_type as 'monthly_quota' | 'pay_as_you_go' | 'free_tier') || 'pay_as_you_go',
|
||||
monthly_quota_usd: props.provider.monthly_quota_usd || undefined,
|
||||
quota_reset_day: props.provider.quota_reset_day || 30,
|
||||
quota_last_reset_at: formatDateTimeLocalInput(props.provider.quota_last_reset_at),
|
||||
quota_expires_at: formatDateTimeLocalInput(props.provider.quota_expires_at),
|
||||
provider_priority: props.provider.provider_priority || 999,
|
||||
keep_priority_on_conversion: props.provider.keep_priority_on_conversion ?? false,
|
||||
is_active: props.provider.is_active,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: undefined,
|
||||
@@ -595,23 +491,6 @@ watch(() => form.value.provider_type, () => {
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 月卡类型必须设置周期开始时间
|
||||
if (form.value.billing_type === 'monthly_quota' && !form.value.quota_last_reset_at) {
|
||||
showError(legacyT('月卡类型必须设置周期开始时间'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
|
||||
const quotaLastResetAt = dateTimeLocalToRfc3339(form.value.quota_last_reset_at)
|
||||
if (form.value.billing_type === 'monthly_quota' && !quotaLastResetAt) {
|
||||
showError(legacyT('周期开始时间必须是合法时间'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
const quotaExpiresAt = dateTimeLocalToRfc3339(form.value.quota_expires_at)
|
||||
if (form.value.quota_expires_at && !quotaExpiresAt) {
|
||||
showError(legacyT('过期时间必须是合法时间'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const currentPoolAdvanced = normalizePoolAdvancedConfig(props.provider?.pool_advanced)
|
||||
@@ -620,12 +499,6 @@ const handleSubmit = async () => {
|
||||
provider_type: form.value.provider_type,
|
||||
description: form.value.description || undefined,
|
||||
website: form.value.website || undefined,
|
||||
billing_type: form.value.billing_type,
|
||||
monthly_quota_usd: form.value.monthly_quota_usd,
|
||||
quota_reset_day: form.value.quota_reset_day,
|
||||
quota_last_reset_at: quotaLastResetAt,
|
||||
quota_expires_at: quotaExpiresAt,
|
||||
keep_priority_on_conversion: form.value.keep_priority_on_conversion,
|
||||
responses_websocket_enabled: form.value.responses_websocket_enabled,
|
||||
is_active: form.value.is_active,
|
||||
// 请求配置
|
||||
|
||||
@@ -98,19 +98,6 @@
|
||||
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 调度策略 -->
|
||||
<button
|
||||
class="group inline-flex items-center gap-1.5 px-2.5 h-8 rounded-md border border-border/50 bg-muted/20 hover:bg-muted/40 hover:border-primary/40 transition-all duration-200 text-xs"
|
||||
:title="legacyT('点击调整调度策略')"
|
||||
@click="$emit('openPriorityDialog')"
|
||||
>
|
||||
<span class="text-muted-foreground/80 hidden sm:inline">{{ legacyT('调度:') }}</span>
|
||||
<span class="font-medium text-foreground/90">{{ priorityModeLabel }}</span>
|
||||
<ChevronDown class="w-3 h-3 text-muted-foreground/70 group-hover:text-foreground transition-colors" />
|
||||
</button>
|
||||
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -141,7 +128,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Search, Plus, ChevronDown, FilterX, Users } from 'lucide-vue-next'
|
||||
import { Search, Plus, FilterX, Users } from 'lucide-vue-next'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
import Select from '@/components/ui/select.vue'
|
||||
@@ -162,7 +149,6 @@ defineProps<{
|
||||
apiFormatFilters: FilterOption[]
|
||||
modelFilters: FilterOption[]
|
||||
hasActiveFilters: boolean
|
||||
priorityModeLabel: string
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
@@ -172,7 +158,6 @@ defineEmits<{
|
||||
'update:filterApiFormat': [value: string]
|
||||
'update:filterModel': [value: string]
|
||||
'resetFilters': []
|
||||
'openPriorityDialog': []
|
||||
'batchProcess': []
|
||||
'addProvider': []
|
||||
'refresh': []
|
||||
|
||||
+65
@@ -137,6 +137,36 @@ function clickButton(text: string) {
|
||||
button.click()
|
||||
}
|
||||
|
||||
const billingFieldNames = [
|
||||
'billing_type',
|
||||
'monthly_quota_usd',
|
||||
'quota_reset_day',
|
||||
'quota_last_reset_at',
|
||||
'quota_expires_at',
|
||||
] as const
|
||||
|
||||
function expectBillingConfigurationHidden() {
|
||||
for (const text of [
|
||||
'计费类型',
|
||||
'月卡额度',
|
||||
'按量付费',
|
||||
'免费套餐',
|
||||
'周期额度 (USD)',
|
||||
'重置周期 (天)',
|
||||
'周期开始时间',
|
||||
'过期时间',
|
||||
]) {
|
||||
expect(document.body.textContent).not.toContain(text)
|
||||
}
|
||||
}
|
||||
|
||||
function expectBillingFieldsOmitted(payload: unknown) {
|
||||
expect(payload).toEqual(expect.any(Object))
|
||||
for (const field of billingFieldNames) {
|
||||
expect(payload).not.toHaveProperty(field)
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
endpointMocks.createProvider.mockReset()
|
||||
endpointMocks.createProvider.mockResolvedValue({ id: 'provider-new', name: 'New Provider' })
|
||||
@@ -226,6 +256,41 @@ describe('ProviderFormDialog transfer limits', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ProviderFormDialog billing configuration', () => {
|
||||
it('hides billing configuration and omits billing fields when creating', async () => {
|
||||
mountDialog(null)
|
||||
await settle()
|
||||
|
||||
expectBillingConfigurationHidden()
|
||||
|
||||
await setInput('#name', 'New Provider')
|
||||
clickButton('创建')
|
||||
await settle()
|
||||
|
||||
expect(endpointMocks.createProvider).toHaveBeenCalledTimes(1)
|
||||
expectBillingFieldsOmitted(endpointMocks.createProvider.mock.calls[0]?.[0])
|
||||
})
|
||||
|
||||
it('hides existing monthly quota configuration and preserves it when editing', async () => {
|
||||
mountDialog(makeProvider({
|
||||
billing_type: 'monthly_quota',
|
||||
monthly_quota_usd: 200,
|
||||
quota_reset_day: 30,
|
||||
quota_last_reset_at: '2026-08-01T00:00:00Z',
|
||||
quota_expires_at: '2026-09-01T00:00:00Z',
|
||||
}))
|
||||
await settle()
|
||||
|
||||
expectBillingConfigurationHidden()
|
||||
|
||||
clickButton('保存')
|
||||
await settle()
|
||||
|
||||
expect(endpointMocks.updateProvider).toHaveBeenCalledTimes(1)
|
||||
expectBillingFieldsOmitted(endpointMocks.updateProvider.mock.calls[0]?.[1])
|
||||
})
|
||||
})
|
||||
|
||||
describe('ProviderFormDialog provider types', () => {
|
||||
it('creates an experimental Claude Code provider from the add dialog', async () => {
|
||||
mountDialog(null)
|
||||
|
||||
@@ -9,7 +9,6 @@ export { default as EndpointFormDialog } from './EndpointFormDialog.vue'
|
||||
export { default as KeyFormDialog } from './KeyFormDialog.vue'
|
||||
export { default as KeyAllowedModelsDialog } from './KeyAllowedModelsDialog.vue'
|
||||
export { default as KeyAllowedModelsEditDialog } from './KeyAllowedModelsEditDialog.vue'
|
||||
export { default as PriorityManagementDialog } from './PriorityManagementDialog.vue'
|
||||
export { default as ProviderModelFormDialog } from './ProviderModelFormDialog.vue'
|
||||
export { default as ProviderDetailDrawer } from './ProviderDetailDrawer.vue'
|
||||
export { default as EndpointHealthTimeline } from './EndpointHealthTimeline.vue'
|
||||
|
||||
Reference in New Issue
Block a user