mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 17:37:47 +08:00
feat(pool): 支持批量导入 Key 和选择性更新设置
This commit is contained in:
@@ -210,30 +210,228 @@
|
||||
<div class="text-xs font-medium text-foreground">
|
||||
执行动作
|
||||
</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
代理节点(仅“配置代理”动作生效)
|
||||
</div>
|
||||
<ProxyNodeSelect
|
||||
:model-value="proxyNodeIdForAction"
|
||||
trigger-class="h-8"
|
||||
@update:model-value="(v: string) => proxyNodeIdForAction = v"
|
||||
/>
|
||||
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<Button
|
||||
v-for="item in ACTION_OPTIONS"
|
||||
:key="item.value"
|
||||
class="h-8 w-full px-3 text-xs"
|
||||
class="h-9 w-full px-3 text-xs"
|
||||
:variant="getActionButtonVariant(item)"
|
||||
:disabled="!canExecuteSpecifiedAction(item.value)"
|
||||
@click="confirmAndExecuteAction(item.value)"
|
||||
:disabled="!canSelectAction(item.value)"
|
||||
@click="handleActionButtonClick(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedAction === 'set_proxy'"
|
||||
class="space-y-2 border-t border-border/60 pt-3"
|
||||
>
|
||||
<div class="text-[11px] text-muted-foreground">选择要绑定的代理节点</div>
|
||||
<ProxyNodeSelect
|
||||
:model-value="proxyNodeIdForAction"
|
||||
trigger-class="h-9"
|
||||
@update:model-value="(v: string) => proxyNodeIdForAction = v"
|
||||
/>
|
||||
<Button
|
||||
class="h-9 w-full text-xs"
|
||||
:disabled="!canExecuteSpecifiedAction('set_proxy')"
|
||||
@click="confirmAndExecuteAction('set_proxy')"
|
||||
>
|
||||
应用代理设置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="selectedAction === 'update_settings'"
|
||||
class="space-y-3 rounded-lg border border-primary/25 bg-primary/5 p-3 sm:p-4"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold">更多设置</h3>
|
||||
<p class="text-[11px] text-muted-foreground">仅更新已勾选字段,未勾选配置保持不变</p>
|
||||
</div>
|
||||
<Badge variant="outline" class="tabular-nums">已选 {{ selectedSettingsCount }} 项</Badge>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="space-y-2 rounded-md border bg-background p-3">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.internal_priority"
|
||||
@update:checked="settingsSelection.internal_priority = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">优先级</Label>
|
||||
</div>
|
||||
<Input
|
||||
v-model.number="settingsDraft.internal_priority"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-9"
|
||||
:disabled="!settingsSelection.internal_priority"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-md border bg-background p-3">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.rpm_limit"
|
||||
@update:checked="settingsSelection.rpm_limit = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">RPM 限制</Label>
|
||||
</div>
|
||||
<Input
|
||||
:model-value="settingsDraft.rpm_limit ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
class="h-9"
|
||||
placeholder="留空为自适应"
|
||||
:disabled="!settingsSelection.rpm_limit"
|
||||
@update:model-value="settingsDraft.rpm_limit = parseNullableNumberInput($event, { min: 1, max: 10000 })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-md border bg-background p-3">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.concurrent_limit"
|
||||
@update:checked="settingsSelection.concurrent_limit = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">并发请求上限</Label>
|
||||
</div>
|
||||
<Input
|
||||
:model-value="settingsDraft.concurrent_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-9"
|
||||
placeholder="留空为不限制"
|
||||
:disabled="!settingsSelection.concurrent_limit"
|
||||
@update:model-value="settingsDraft.concurrent_limit = parseNullableNumberInput($event, { min: 0 })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-md border bg-background p-3">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.cache_ttl_minutes"
|
||||
@update:checked="settingsSelection.cache_ttl_minutes = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">缓存 TTL(分钟)</Label>
|
||||
</div>
|
||||
<Input
|
||||
:model-value="settingsDraft.cache_ttl_minutes"
|
||||
type="number"
|
||||
min="0"
|
||||
max="60"
|
||||
class="h-9"
|
||||
:disabled="!settingsSelection.cache_ttl_minutes"
|
||||
@update:model-value="settingsDraft.cache_ttl_minutes = parseNumberInput($event, { min: 0, max: 60 }) ?? 5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-md border bg-background p-3">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.max_probe_interval_minutes"
|
||||
@update:checked="settingsSelection.max_probe_interval_minutes = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">熔断探测(分钟)</Label>
|
||||
</div>
|
||||
<Input
|
||||
:model-value="settingsDraft.max_probe_interval_minutes"
|
||||
type="number"
|
||||
min="0"
|
||||
max="32"
|
||||
class="h-9"
|
||||
:disabled="!settingsSelection.max_probe_interval_minutes"
|
||||
@update:model-value="settingsDraft.max_probe_interval_minutes = parseNumberInput($event, { min: 0, max: 32 }) ?? 32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-md border bg-background p-3">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.is_active"
|
||||
@update:checked="settingsSelection.is_active = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">启用状态</Label>
|
||||
</div>
|
||||
<Select v-model="settingsStatus" :disabled="!settingsSelection.is_active">
|
||||
<SelectTrigger class="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="enabled">启用</SelectItem>
|
||||
<SelectItem value="disabled">停用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-md border bg-background p-3 sm:col-span-2">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.proxy_node_id"
|
||||
@update:checked="settingsSelection.proxy_node_id = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">账号代理</Label>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-[9rem_minmax(0,1fr)]">
|
||||
<Select v-model="settingsDraft.proxy_mode" :disabled="!settingsSelection.proxy_node_id">
|
||||
<SelectTrigger class="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="set">设置节点</SelectItem>
|
||||
<SelectItem value="clear">清除代理</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ProxyNodeSelect
|
||||
v-if="settingsDraft.proxy_mode === 'set'"
|
||||
:model-value="settingsDraft.proxy_node_id"
|
||||
trigger-class="h-9"
|
||||
:class="!settingsSelection.proxy_node_id ? 'pointer-events-none opacity-50' : ''"
|
||||
@update:model-value="(value: string) => settingsDraft.proxy_node_id = value"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-9 items-center rounded-md border bg-muted/30 px-3 text-xs text-muted-foreground"
|
||||
>
|
||||
回退到 Provider 默认代理
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-md border bg-background p-3">
|
||||
<div class="flex min-h-6 items-center gap-2">
|
||||
<Checkbox
|
||||
:checked="settingsSelection.note"
|
||||
@update:checked="settingsSelection.note = $event === true"
|
||||
/>
|
||||
<Label class="text-xs">备注</Label>
|
||||
</div>
|
||||
<Input
|
||||
v-model="settingsDraft.note"
|
||||
class="h-9"
|
||||
placeholder="留空清除备注"
|
||||
:disabled="!settingsSelection.note"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="min-h-5 text-xs text-destructive">
|
||||
{{ settingsErrors[0] || '' }}
|
||||
</p>
|
||||
<Button
|
||||
class="min-h-10 sm:min-w-36"
|
||||
:disabled="!canExecuteSpecifiedAction('update_settings')"
|
||||
@click="confirmAndExecuteAction('update_settings')"
|
||||
>
|
||||
应用更多设置
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="executing && progressTotal > 0"
|
||||
class="space-y-1"
|
||||
@@ -270,8 +468,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { Dialog, Button, Input, Checkbox, Badge } from '@/components/ui'
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
|
||||
import { RefreshCw, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
@@ -302,6 +512,13 @@ import {
|
||||
} from '@/utils/providerKeyStatus'
|
||||
import { getQuotaDisplayText } from '@/utils/providerKeyQuota'
|
||||
import { runChunkedBatchAction } from '@/utils/batchAction'
|
||||
import { parseNullableNumberInput, parseNumberInput } from '@/utils/form'
|
||||
import {
|
||||
buildPoolKeySettingsPatch,
|
||||
createPoolKeyBatchSettingSelection,
|
||||
createPoolKeyBatchSettingsDraft,
|
||||
validatePoolKeyBatchSettings,
|
||||
} from '@/features/pool/utils/poolKeyBatchSettings'
|
||||
|
||||
type QuickSelectorValue =
|
||||
| 'banned'
|
||||
@@ -322,6 +539,7 @@ type BatchActionValue =
|
||||
| 'refresh_quota'
|
||||
| 'clear_proxy'
|
||||
| 'set_proxy'
|
||||
| 'update_settings'
|
||||
| 'enable'
|
||||
| 'disable'
|
||||
|
||||
@@ -373,6 +591,7 @@ const ACTION_OPTIONS: BatchActionOption[] = [
|
||||
{ value: 'refresh_quota', label: '刷新额度', hint: '调用额度刷新接口,适合核对最新配额状态。' },
|
||||
{ value: 'refresh_oauth', label: '刷新 OAuth', hint: '仅对 OAuth 账号有效,非 OAuth 账号会自动跳过。' },
|
||||
{ value: 'set_proxy', label: '配置代理', hint: '为选中账号绑定独立代理节点。' },
|
||||
{ value: 'update_settings', label: '更多设置', hint: '选择性修改 RPM、并发、熔断、备注和代理等配置。' },
|
||||
{ value: 'clear_proxy', label: '清除代理', hint: '移除账号独立代理,回退到提供商默认代理。' },
|
||||
{ value: 'enable', label: '启用', hint: '批量启用账号,恢复可调度状态。' },
|
||||
{ value: 'disable', label: '禁用', hint: '批量禁用账号,保留数据但停止调度。' },
|
||||
@@ -394,6 +613,12 @@ const selectAllFiltered = ref(false)
|
||||
const searchText = ref('')
|
||||
const selectedAction = ref<BatchActionValue>('refresh_quota')
|
||||
const proxyNodeIdForAction = ref('')
|
||||
const settingsSelection = reactive(createPoolKeyBatchSettingSelection())
|
||||
const settingsDraft = reactive(createPoolKeyBatchSettingsDraft())
|
||||
const settingsStatus = computed({
|
||||
get: () => settingsDraft.is_active ? 'enabled' : 'disabled',
|
||||
set: (value: string) => { settingsDraft.is_active = value === 'enabled' },
|
||||
})
|
||||
const lastResultMessage = ref('')
|
||||
const progressTotal = ref(0)
|
||||
const progressDone = ref(0)
|
||||
@@ -448,6 +673,8 @@ const isCurrentPageFullySelected = computed(() => {
|
||||
})
|
||||
const canClearSelection = computed(() => selectAllFiltered.value || selectedKeyIds.value.length > 0)
|
||||
const activeQuickSelectorSet = computed(() => new Set(activeQuickSelectors.value))
|
||||
const settingsErrors = computed(() => validatePoolKeyBatchSettings(settingsSelection, settingsDraft))
|
||||
const selectedSettingsCount = computed(() => Object.values(settingsSelection).filter(Boolean).length)
|
||||
|
||||
function sanitizeFileNamePart(value: unknown, fallback: string): string {
|
||||
const sanitized = String(value || '')
|
||||
@@ -706,14 +933,33 @@ function toggleQuickSelector(selector: QuickSelectorValue): void {
|
||||
function canExecuteSpecifiedAction(action: BatchActionValue): boolean {
|
||||
if (executing.value || loading.value || selectedCount.value === 0) return false
|
||||
if (action === 'set_proxy') return Boolean(proxyNodeIdForAction.value)
|
||||
if (action === 'update_settings') return settingsErrors.value.length === 0
|
||||
return true
|
||||
}
|
||||
|
||||
function canSelectAction(action: BatchActionValue): boolean {
|
||||
if (executing.value || loading.value) return false
|
||||
if (action === 'set_proxy' || action === 'update_settings') return true
|
||||
return selectedCount.value > 0
|
||||
}
|
||||
|
||||
function getActionButtonVariant(option: BatchActionOption): 'default' | 'destructive' | 'outline' {
|
||||
if (option.destructive) return 'destructive'
|
||||
if (
|
||||
option.value === selectedAction.value
|
||||
&& (option.value === 'set_proxy' || option.value === 'update_settings')
|
||||
) return 'default'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function handleActionButtonClick(action: BatchActionValue): void {
|
||||
if (action === 'set_proxy' || action === 'update_settings') {
|
||||
selectedAction.value = action
|
||||
return
|
||||
}
|
||||
void confirmAndExecuteAction(action)
|
||||
}
|
||||
|
||||
async function confirmAndExecuteAction(action: BatchActionValue): Promise<void> {
|
||||
selectedAction.value = action
|
||||
if (selectedCount.value === 0) {
|
||||
@@ -724,6 +970,10 @@ async function confirmAndExecuteAction(action: BatchActionValue): Promise<void>
|
||||
warning('请先选择代理节点')
|
||||
return
|
||||
}
|
||||
if (action === 'update_settings' && settingsErrors.value.length > 0) {
|
||||
warning(settingsErrors.value[0])
|
||||
return
|
||||
}
|
||||
if (!canExecuteSpecifiedAction(action)) return
|
||||
|
||||
const actionOption = ACTION_OPTIONS.find((item) => item.value === action)
|
||||
@@ -809,6 +1059,10 @@ async function executeAction(actionOverride?: BatchActionValue): Promise<void> {
|
||||
warning('请先选择代理节点')
|
||||
return
|
||||
}
|
||||
if (selectedAction.value === 'update_settings' && settingsErrors.value.length > 0) {
|
||||
warning(settingsErrors.value[0])
|
||||
return
|
||||
}
|
||||
|
||||
executing.value = true
|
||||
let successCount = 0
|
||||
@@ -930,7 +1184,7 @@ async function executeAction(actionOverride?: BatchActionValue): Promise<void> {
|
||||
|
||||
progressDone.value = Math.min(i + BATCH_SIZE, targetIds.length)
|
||||
}
|
||||
} else if (['enable', 'disable', 'clear_proxy', 'set_proxy'].includes(selectedAction.value)) {
|
||||
} else if (['enable', 'disable', 'clear_proxy', 'set_proxy', 'update_settings'].includes(selectedAction.value)) {
|
||||
const targetIds = selectedKeys.map((key) => key.key_id)
|
||||
const BATCH_SIZE = 2000
|
||||
const totalBatches = Math.ceil(targetIds.length / BATCH_SIZE)
|
||||
@@ -944,12 +1198,14 @@ async function executeAction(actionOverride?: BatchActionValue): Promise<void> {
|
||||
|
||||
const payload = selectedAction.value === 'set_proxy'
|
||||
? { node_id: proxyNodeIdForAction.value, enabled: true }
|
||||
: undefined
|
||||
: selectedAction.value === 'update_settings'
|
||||
? buildPoolKeySettingsPatch(settingsSelection, settingsDraft)
|
||||
: undefined
|
||||
|
||||
try {
|
||||
const result = await batchActionPoolKeys(props.providerId, {
|
||||
key_ids: batch,
|
||||
action: selectedAction.value as 'enable' | 'disable' | 'clear_proxy' | 'set_proxy',
|
||||
action: selectedAction.value as 'enable' | 'disable' | 'clear_proxy' | 'set_proxy' | 'update_settings',
|
||||
...(payload ? { payload } : {}),
|
||||
})
|
||||
successCount += result.affected
|
||||
@@ -1049,6 +1305,8 @@ watch(
|
||||
activeQuickSelectors.value = []
|
||||
selectedAction.value = 'refresh_quota'
|
||||
proxyNodeIdForAction.value = ''
|
||||
Object.assign(settingsSelection, createPoolKeyBatchSettingSelection())
|
||||
Object.assign(settingsDraft, createPoolKeyBatchSettingsDraft())
|
||||
resetSelection(true)
|
||||
filteredTotal.value = 0
|
||||
pageKeys.value = []
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
buildPoolKeySettingsPatch,
|
||||
createPoolKeyBatchSettingSelection,
|
||||
createPoolKeyBatchSettingsDraft,
|
||||
validatePoolKeyBatchSettings,
|
||||
} from '../poolKeyBatchSettings'
|
||||
|
||||
describe('pool key batch settings', () => {
|
||||
it('only includes explicitly selected fields', () => {
|
||||
const selection = createPoolKeyBatchSettingSelection()
|
||||
const draft = createPoolKeyBatchSettingsDraft()
|
||||
selection.rpm_limit = true
|
||||
selection.proxy_node_id = true
|
||||
draft.rpm_limit = 800
|
||||
draft.proxy_mode = 'clear'
|
||||
|
||||
expect(buildPoolKeySettingsPatch(selection, draft)).toEqual({
|
||||
rpm_limit: 800,
|
||||
proxy_node_id: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('supports adaptive RPM and unlimited concurrency', () => {
|
||||
const selection = createPoolKeyBatchSettingSelection()
|
||||
const draft = createPoolKeyBatchSettingsDraft()
|
||||
selection.rpm_limit = true
|
||||
selection.concurrent_limit = true
|
||||
|
||||
expect(validatePoolKeyBatchSettings(selection, draft)).toEqual([])
|
||||
expect(buildPoolKeySettingsPatch(selection, draft)).toEqual({
|
||||
rpm_limit: null,
|
||||
concurrent_limit: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('requires a selected field and a proxy node for set mode', () => {
|
||||
const selection = createPoolKeyBatchSettingSelection()
|
||||
const draft = createPoolKeyBatchSettingsDraft()
|
||||
|
||||
expect(validatePoolKeyBatchSettings(selection, draft)).toEqual(['请至少选择一个要修改的设置'])
|
||||
|
||||
selection.proxy_node_id = true
|
||||
expect(validatePoolKeyBatchSettings(selection, draft)).toEqual(['请选择要设置的代理节点'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { PoolKeySettingsPatch } from '@/api/endpoints/pool'
|
||||
|
||||
export type PoolKeyBatchSettingField =
|
||||
| 'internal_priority'
|
||||
| 'rpm_limit'
|
||||
| 'concurrent_limit'
|
||||
| 'cache_ttl_minutes'
|
||||
| 'max_probe_interval_minutes'
|
||||
| 'is_active'
|
||||
| 'note'
|
||||
| 'proxy_node_id'
|
||||
|
||||
export type PoolKeyBatchSettingSelection = Record<PoolKeyBatchSettingField, boolean>
|
||||
|
||||
export interface PoolKeyBatchSettingsDraft {
|
||||
internal_priority: number
|
||||
rpm_limit: number | null
|
||||
concurrent_limit: number | null
|
||||
cache_ttl_minutes: number
|
||||
max_probe_interval_minutes: number
|
||||
is_active: boolean
|
||||
note: string
|
||||
proxy_mode: 'set' | 'clear'
|
||||
proxy_node_id: string
|
||||
}
|
||||
|
||||
export function createPoolKeyBatchSettingSelection(): PoolKeyBatchSettingSelection {
|
||||
return {
|
||||
internal_priority: false,
|
||||
rpm_limit: false,
|
||||
concurrent_limit: false,
|
||||
cache_ttl_minutes: false,
|
||||
max_probe_interval_minutes: false,
|
||||
is_active: false,
|
||||
note: false,
|
||||
proxy_node_id: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function createPoolKeyBatchSettingsDraft(): PoolKeyBatchSettingsDraft {
|
||||
return {
|
||||
internal_priority: 50,
|
||||
rpm_limit: null,
|
||||
concurrent_limit: null,
|
||||
cache_ttl_minutes: 5,
|
||||
max_probe_interval_minutes: 32,
|
||||
is_active: true,
|
||||
note: '',
|
||||
proxy_mode: 'set',
|
||||
proxy_node_id: '',
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePoolKeyBatchSettings(
|
||||
selection: PoolKeyBatchSettingSelection,
|
||||
draft: PoolKeyBatchSettingsDraft,
|
||||
): string[] {
|
||||
const errors: string[] = []
|
||||
if (!Object.values(selection).some(Boolean)) errors.push('请至少选择一个要修改的设置')
|
||||
if (selection.internal_priority && (!Number.isInteger(draft.internal_priority) || draft.internal_priority < 0)) {
|
||||
errors.push('优先级必须是大于等于 0 的整数')
|
||||
}
|
||||
if (selection.rpm_limit && draft.rpm_limit !== null && (
|
||||
!Number.isInteger(draft.rpm_limit) || draft.rpm_limit < 1 || draft.rpm_limit > 10_000
|
||||
)) {
|
||||
errors.push('RPM 必须在 1 到 10000 之间,留空表示自适应')
|
||||
}
|
||||
if (selection.concurrent_limit && draft.concurrent_limit !== null && (
|
||||
!Number.isInteger(draft.concurrent_limit) || draft.concurrent_limit < 0
|
||||
)) {
|
||||
errors.push('并发上限必须是大于等于 0 的整数')
|
||||
}
|
||||
if (selection.cache_ttl_minutes && (
|
||||
!Number.isInteger(draft.cache_ttl_minutes) || draft.cache_ttl_minutes < 0 || draft.cache_ttl_minutes > 60
|
||||
)) {
|
||||
errors.push('缓存 TTL 必须在 0 到 60 分钟之间')
|
||||
}
|
||||
if (selection.max_probe_interval_minutes && (
|
||||
!Number.isInteger(draft.max_probe_interval_minutes)
|
||||
|| draft.max_probe_interval_minutes < 0
|
||||
|| draft.max_probe_interval_minutes > 32
|
||||
)) {
|
||||
errors.push('熔断探测必须在 0 到 32 分钟之间')
|
||||
}
|
||||
if (selection.proxy_node_id && draft.proxy_mode === 'set' && !draft.proxy_node_id.trim()) {
|
||||
errors.push('请选择要设置的代理节点')
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
export function buildPoolKeySettingsPatch(
|
||||
selection: PoolKeyBatchSettingSelection,
|
||||
draft: PoolKeyBatchSettingsDraft,
|
||||
): PoolKeySettingsPatch {
|
||||
const patch: PoolKeySettingsPatch = {}
|
||||
if (selection.internal_priority) patch.internal_priority = draft.internal_priority
|
||||
if (selection.rpm_limit) patch.rpm_limit = draft.rpm_limit
|
||||
if (selection.concurrent_limit) patch.concurrent_limit = draft.concurrent_limit
|
||||
if (selection.cache_ttl_minutes) patch.cache_ttl_minutes = draft.cache_ttl_minutes
|
||||
if (selection.max_probe_interval_minutes) {
|
||||
patch.max_probe_interval_minutes = draft.max_probe_interval_minutes
|
||||
}
|
||||
if (selection.is_active) patch.is_active = draft.is_active
|
||||
if (selection.note) patch.note = draft.note.trim() || null
|
||||
if (selection.proxy_node_id) {
|
||||
patch.proxy_node_id = draft.proxy_mode === 'clear' ? null : draft.proxy_node_id.trim()
|
||||
}
|
||||
return patch
|
||||
}
|
||||
Reference in New Issue
Block a user