mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-12 04:28:28 +08:00
refactor: 重构限流系统和健康监控,支持按 API 格式区分
- 将 adaptive_concurrency 重命名为 adaptive_rpm,从并发控制改为 RPM 控制 - 健康监控器支持按 API 格式独立管理健康度和熔断器状态 - 新增 model_permissions 模块,支持按格式配置允许的模型 - 重构前端提供商相关表单组件,新增 Collapsible UI 组件 - 新增数据库迁移脚本支持新的数据结构
This commit is contained in:
@@ -4,47 +4,29 @@
|
||||
:title="isEditMode ? '编辑提供商' : '添加提供商'"
|
||||
:description="isEditMode ? '更新提供商配置。API 端点和密钥需在详情页面单独管理。' : '创建新的提供商配置。创建后可以为其添加 API 端点和密钥。'"
|
||||
:icon="isEditMode ? SquarePen : Server"
|
||||
size="2xl"
|
||||
size="xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
<form
|
||||
class="space-y-6"
|
||||
class="space-y-5"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
基本信息
|
||||
</h3>
|
||||
|
||||
<!-- 添加模式显示提供商标识 -->
|
||||
<div
|
||||
v-if="!isEditMode"
|
||||
class="space-y-2"
|
||||
>
|
||||
<Label for="name">提供商标识 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
placeholder="例如: openai-primary"
|
||||
required
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
唯一ID,创建后不可修改
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="display_name">显示名称 *</Label>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="name">名称 *</Label>
|
||||
<Input
|
||||
id="display_name"
|
||||
v-model="form.display_name"
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
placeholder="例如: OpenAI 主账号"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="website">主站链接</Label>
|
||||
<Input
|
||||
id="website"
|
||||
@@ -55,24 +37,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="description">描述</Label>
|
||||
<Textarea
|
||||
<Input
|
||||
id="description"
|
||||
v-model="form.description"
|
||||
placeholder="提供商描述(可选)"
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 计费与限流 -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
计费与限流
|
||||
</h3>
|
||||
<!-- 计费与限流 / 请求配置 -->
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
计费与限流
|
||||
</h3>
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
请求配置
|
||||
</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label>计费类型</Label>
|
||||
<Select
|
||||
v-model="form.billing_type"
|
||||
@@ -82,27 +68,35 @@
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly_quota">
|
||||
月卡额度
|
||||
</SelectItem>
|
||||
<SelectItem value="pay_as_you_go">
|
||||
按量付费
|
||||
</SelectItem>
|
||||
<SelectItem value="free_tier">
|
||||
免费套餐
|
||||
</SelectItem>
|
||||
<SelectItem value="monthly_quota">月卡额度</SelectItem>
|
||||
<SelectItem value="pay_as_you_go">按量付费</SelectItem>
|
||||
<SelectItem value="free_tier">免费套餐</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>RPM 限制</Label>
|
||||
<Input
|
||||
:model-value="form.rpm_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="不限制请留空"
|
||||
@update:model-value="(v) => form.rpm_limit = parseNumberInput(v)"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label>超时时间 (秒)</Label>
|
||||
<Input
|
||||
:model-value="form.timeout ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="600"
|
||||
placeholder="默认 300"
|
||||
@update:model-value="(v) => form.timeout = parseNumberInput(v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>最大重试次数</Label>
|
||||
<Input
|
||||
:model-value="form.max_retries ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
placeholder="默认 2"
|
||||
@update:model-value="(v) => form.max_retries = parseNumberInput(v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,52 +105,94 @@
|
||||
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-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">周期额度 (USD)</Label>
|
||||
<Input
|
||||
:model-value="form.monthly_quota_usd ?? ''"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="h-9"
|
||||
@update:model-value="(v) => form.monthly_quota_usd = parseNumberInput(v, { allowFloat: true })"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">重置周期 (天)</Label>
|
||||
<Input
|
||||
:model-value="form.quota_reset_day ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="365"
|
||||
class="h-9"
|
||||
@update:model-value="(v) => form.quota_reset_day = parseNumberInput(v) ?? 30"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">
|
||||
周期开始时间
|
||||
<span class="text-red-500">*</span>
|
||||
周期开始时间 <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
v-model="form.quota_last_reset_at"
|
||||
type="datetime-local"
|
||||
class="h-9"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
系统会自动统计从该时间点开始的使用量
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">过期时间</Label>
|
||||
<Input
|
||||
v-model="form.quota_expires_at"
|
||||
type="datetime-local"
|
||||
class="h-9"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空表示永久有效
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代理配置 -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium">
|
||||
代理配置
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
:model-value="form.proxy_enabled"
|
||||
@update:model-value="(v: boolean) => form.proxy_enabled = v"
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground">启用代理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="form.proxy_enabled"
|
||||
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">代理地址 *</Label>
|
||||
<Input
|
||||
v-model="form.proxy_url"
|
||||
placeholder="http://proxy:port 或 socks5://proxy:port"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">用户名</Label>
|
||||
<Input
|
||||
v-model="form.proxy_username"
|
||||
placeholder="可选"
|
||||
autocomplete="off"
|
||||
data-form-type="other"
|
||||
data-lpignore="true"
|
||||
data-1p-ignore="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">密码</Label>
|
||||
<Input
|
||||
v-model="form.proxy_password"
|
||||
type="password"
|
||||
placeholder="可选"
|
||||
autocomplete="new-password"
|
||||
data-form-type="other"
|
||||
data-lpignore="true"
|
||||
data-1p-ignore="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +208,7 @@
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="loading || !form.display_name || (!isEditMode && !form.name)"
|
||||
:disabled="loading || !form.name"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存' : '创建') }}
|
||||
@@ -187,13 +223,13 @@ import {
|
||||
Dialog,
|
||||
Button,
|
||||
Input,
|
||||
Textarea,
|
||||
Label,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
Switch,
|
||||
} from '@/components/ui'
|
||||
import { Server, SquarePen } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
@@ -223,7 +259,6 @@ const internalOpen = computed(() => props.modelValue)
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
display_name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
// 计费配置
|
||||
@@ -232,19 +267,25 @@ const form = ref({
|
||||
quota_reset_day: 30,
|
||||
quota_last_reset_at: '', // 周期开始时间
|
||||
quota_expires_at: '',
|
||||
rpm_limit: undefined as string | number | undefined,
|
||||
provider_priority: 999,
|
||||
// 状态配置
|
||||
is_active: true,
|
||||
rate_limit: undefined as number | undefined,
|
||||
concurrent_limit: undefined as number | undefined,
|
||||
// 请求配置
|
||||
timeout: undefined as number | undefined,
|
||||
max_retries: undefined as number | undefined,
|
||||
// 代理配置(扁平化便于表单绑定)
|
||||
proxy_enabled: false,
|
||||
proxy_url: '',
|
||||
proxy_username: '',
|
||||
proxy_password: '',
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
form.value = {
|
||||
name: '',
|
||||
display_name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
billing_type: 'pay_as_you_go',
|
||||
@@ -252,11 +293,18 @@ function resetForm() {
|
||||
quota_reset_day: 30,
|
||||
quota_last_reset_at: '',
|
||||
quota_expires_at: '',
|
||||
rpm_limit: undefined,
|
||||
provider_priority: 999,
|
||||
is_active: true,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: undefined,
|
||||
// 请求配置
|
||||
timeout: undefined,
|
||||
max_retries: undefined,
|
||||
// 代理配置
|
||||
proxy_enabled: false,
|
||||
proxy_url: '',
|
||||
proxy_username: '',
|
||||
proxy_password: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,9 +312,9 @@ function resetForm() {
|
||||
function loadProviderData() {
|
||||
if (!props.provider) return
|
||||
|
||||
const proxy = props.provider.proxy
|
||||
form.value = {
|
||||
name: props.provider.name,
|
||||
display_name: props.provider.display_name,
|
||||
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',
|
||||
@@ -276,11 +324,18 @@ function loadProviderData() {
|
||||
new Date(props.provider.quota_last_reset_at).toISOString().slice(0, 16) : '',
|
||||
quota_expires_at: props.provider.quota_expires_at ?
|
||||
new Date(props.provider.quota_expires_at).toISOString().slice(0, 16) : '',
|
||||
rpm_limit: props.provider.rpm_limit ?? undefined,
|
||||
provider_priority: props.provider.provider_priority || 999,
|
||||
is_active: props.provider.is_active,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: undefined,
|
||||
// 请求配置
|
||||
timeout: props.provider.timeout ?? undefined,
|
||||
max_retries: props.provider.max_retries ?? undefined,
|
||||
// 代理配置
|
||||
proxy_enabled: proxy?.enabled ?? false,
|
||||
proxy_url: proxy?.url || '',
|
||||
proxy_username: proxy?.username || '',
|
||||
proxy_password: proxy?.password || '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,17 +357,37 @@ const handleSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 启用代理时必须填写代理地址
|
||||
if (form.value.proxy_enabled && !form.value.proxy_url) {
|
||||
showError('启用代理时必须填写代理地址', '验证失败')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 构建代理配置
|
||||
const proxy = form.value.proxy_enabled ? {
|
||||
url: form.value.proxy_url,
|
||||
username: form.value.proxy_username || undefined,
|
||||
password: form.value.proxy_password || undefined,
|
||||
enabled: true,
|
||||
} : null
|
||||
|
||||
const payload = {
|
||||
...form.value,
|
||||
rpm_limit:
|
||||
form.value.rpm_limit === undefined || form.value.rpm_limit === ''
|
||||
? null
|
||||
: Number(form.value.rpm_limit),
|
||||
// 空字符串时不发送
|
||||
name: form.value.name,
|
||||
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: form.value.quota_last_reset_at || undefined,
|
||||
quota_expires_at: form.value.quota_expires_at || undefined,
|
||||
provider_priority: form.value.provider_priority,
|
||||
is_active: form.value.is_active,
|
||||
// 请求配置
|
||||
timeout: form.value.timeout ?? undefined,
|
||||
max_retries: form.value.max_retries ?? undefined,
|
||||
proxy,
|
||||
}
|
||||
|
||||
if (isEditMode.value && props.provider) {
|
||||
|
||||
Reference in New Issue
Block a user