fix: preserve provider auth metadata

This commit is contained in:
elky
2026-06-20 22:11:42 +08:00
parent 1123095bb7
commit f1c30439ff
51 changed files with 2098 additions and 420 deletions
@@ -2,7 +2,7 @@
<Dialog
:model-value="modelValue"
title="高级设置"
description="冷却、健康、成本控制与其他高级参数"
description="冷却、热池与其他高级参数"
size="3xl"
@update:model-value="emit('update:modelValue', $event)"
>
@@ -11,18 +11,18 @@
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold">
冷却与健康
冷却与热池
</h3>
<span class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
核心策略
</span>
</div>
<p class="text-xs leading-5 text-muted-foreground">
控制自动冷却、自适应热池、异常清理和全局调度优先级。
控制冷却时间、自适应热池和异常清理。
</p>
</div>
<div class="grid gap-3 lg:grid-cols-3">
<div class="grid gap-3 lg:grid-cols-2">
<div
v-for="item in healthToggleCards"
:key="item.key"
@@ -134,175 +134,83 @@
@update:model-value="(v) => form.overload_cooldown_seconds = parseNum(v)"
/>
</div>
</div>
</section>
<section class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4 sm:p-5">
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold">
批量操作
</h3>
<span class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
任务效率
</span>
</div>
<p class="text-xs leading-5 text-muted-foreground">
控制刷新 OAuth、自适应热池和批量额度处理时的并行请求数。
</p>
</div>
<div class="grid gap-3 rounded-xl bg-muted/30 p-4 sm:grid-cols-2 xl:grid-cols-4">
<div class="space-y-1.5">
<Label>
粘性会话 TTL
<span class="text-xs text-muted-foreground">(秒)</span>
并发数
</Label>
<Input
:model-value="form.sticky_session_ttl_seconds ?? ''"
:model-value="form.batch_concurrency ?? ''"
type="number"
min="60"
max="86400"
placeholder="3600 (留空禁用)"
@update:model-value="(v) => form.sticky_session_ttl_seconds = parseNum(v)"
min="1"
max="32"
placeholder="8"
@update:model-value="(v) => form.batch_concurrency = parseNum(v)"
/>
<p class="text-[11px] leading-5 text-muted-foreground">
为空时沿用默认值;数值越大,批量操作越快,但会增加瞬时请求压力。
</p>
</div>
<div class="space-y-1.5">
<Label>
探测并发
</Label>
<Input
:model-value="form.probe_concurrency ?? ''"
type="number"
min="1"
max="64"
placeholder="4"
@update:model-value="(v) => form.probe_concurrency = parseNum(v)"
/>
</div>
<div class="space-y-1.5">
<Label>
全局优先级
评分 Top-N
</Label>
<Input
:model-value="form.global_priority ?? ''"
:model-value="form.score_top_n ?? ''"
type="number"
min="0"
max="999999"
placeholder="留空回退 provider_priority"
@update:model-value="(v) => form.global_priority = parseNum(v)"
min="1"
max="4096"
placeholder="128"
@update:model-value="(v) => form.score_top_n = parseNum(v)"
/>
</div>
<div class="space-y-1.5">
<Label>
回退扫描
</Label>
<Input
:model-value="form.score_fallback_scan_limit ?? ''"
type="number"
min="1"
max="100000"
placeholder="1024"
@update:model-value="(v) => form.score_fallback_scan_limit = parseNum(v)"
/>
</div>
</div>
</section>
<div :class="secondarySectionLayout.wrapperClass">
<section class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4 sm:p-5">
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold">
成本控制
</h3>
<span class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
额度保护
</span>
</div>
<p class="text-xs leading-5 text-muted-foreground">
控制窗口期、Key 限额与软阈值,防止个别账号短时间内过度消耗。
</p>
</div>
<div
class="grid gap-3 sm:grid-cols-2"
:class="costFieldLayout.desktopColumnsClass"
>
<div class="space-y-1.5">
<Label>
成本窗口
<span class="text-xs text-muted-foreground">(秒)</span>
</Label>
<Input
:model-value="form.cost_window_seconds ?? ''"
type="number"
min="3600"
max="86400"
placeholder="18000 (5 小时)"
@update:model-value="(v) => form.cost_window_seconds = parseNum(v)"
/>
</div>
<div class="space-y-1.5">
<Label>
Key 窗口限额
<span class="text-xs text-muted-foreground">(tokens)</span>
</Label>
<Input
:model-value="form.cost_limit_per_key_tokens ?? ''"
type="number"
min="0"
placeholder="留空 = 不限"
@update:model-value="(v) => form.cost_limit_per_key_tokens = parseNum(v)"
/>
</div>
<div class="space-y-1.5">
<Label>
软阈值
<span class="text-xs text-muted-foreground">(%)</span>
</Label>
<Input
:model-value="form.cost_soft_threshold_percent ?? ''"
type="number"
min="0"
max="100"
placeholder="80"
@update:model-value="(v) => form.cost_soft_threshold_percent = parseNum(v)"
/>
</div>
</div>
</section>
<section class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4 sm:p-5">
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold">
批量操作
</h3>
<span class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
任务效率
</span>
</div>
<p class="text-xs leading-5 text-muted-foreground">
控制刷新 OAuth、自适应热池和批量额度处理时的并行请求数。
</p>
</div>
<div class="grid gap-3 rounded-xl bg-muted/30 p-4 sm:grid-cols-2">
<div class="space-y-1.5">
<Label>
并发数
</Label>
<Input
:model-value="form.batch_concurrency ?? ''"
type="number"
min="1"
max="32"
placeholder="8"
@update:model-value="(v) => form.batch_concurrency = parseNum(v)"
/>
<p class="text-[11px] leading-5 text-muted-foreground">
为空时沿用默认值;数值越大,批量操作越快,但会增加瞬时请求压力。
</p>
</div>
<div class="space-y-1.5">
<Label>
探测并发
</Label>
<Input
:model-value="form.probe_concurrency ?? ''"
type="number"
min="1"
max="64"
placeholder="4"
@update:model-value="(v) => form.probe_concurrency = parseNum(v)"
/>
</div>
<div class="space-y-1.5">
<Label>
评分 Top-N
</Label>
<Input
:model-value="form.score_top_n ?? ''"
type="number"
min="1"
max="4096"
placeholder="128"
@update:model-value="(v) => form.score_top_n = parseNum(v)"
/>
</div>
<div class="space-y-1.5">
<Label>
回退扫描
</Label>
<Input
:model-value="form.score_fallback_scan_limit ?? ''"
type="number"
min="1"
max="100000"
placeholder="1024"
@update:model-value="(v) => form.score_fallback_scan_limit = parseNum(v)"
/>
</div>
</div>
</section>
</div>
<section class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4 sm:p-5">
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
@@ -625,9 +533,7 @@ import { parseApiError } from '@/utils/errorParser'
import { updateProvider } from '@/api/endpoints'
import {
buildPoolCooldownFieldLayout,
buildPoolCostFieldLayout,
buildPoolHealthToggleCards,
buildPoolSecondarySectionLayout,
type PoolHealthToggleKey,
} from '@/features/pool/utils/poolAdvancedDialog'
import type {
@@ -658,18 +564,10 @@ const isClaudeCode = computed(() => {
const healthToggleCards = buildPoolHealthToggleCards()
const cooldownFieldLayout = buildPoolCooldownFieldLayout()
const costFieldLayout = buildPoolCostFieldLayout()
const secondarySectionLayout = buildPoolSecondarySectionLayout()
const form = ref({
global_priority: null as number | null | undefined,
sticky_session_ttl_seconds: null as number | null | undefined,
health_policy_enabled: true,
rate_limit_cooldown_seconds: null as number | null | undefined,
overload_cooldown_seconds: null as number | null | undefined,
cost_window_seconds: null as number | null | undefined,
cost_limit_per_key_tokens: null as number | null | undefined,
cost_soft_threshold_percent: null as number | null | undefined,
batch_concurrency: null as number | null | undefined,
probe_concurrency: null as number | null | undefined,
score_top_n: null as number | null | undefined,
@@ -721,8 +619,6 @@ function parseNum(v: string | number): number | undefined {
function getHealthToggleValue(key: PoolHealthToggleKey): boolean {
switch (key) {
case 'health_policy_enabled':
return form.value.health_policy_enabled
case 'probing_enabled':
return form.value.probing_enabled
case 'account_self_check_enabled':
@@ -736,9 +632,6 @@ function getHealthToggleValue(key: PoolHealthToggleKey): boolean {
function updateHealthToggleValue(key: PoolHealthToggleKey, value: boolean): void {
switch (key) {
case 'health_policy_enabled':
form.value.health_policy_enabled = value
return
case 'probing_enabled':
form.value.probing_enabled = value
return
@@ -760,14 +653,8 @@ watch(() => props.modelValue, (open) => {
const scoreRules = cfg?.score_rules
const scoreWeights = scoreRules?.weights
form.value = {
global_priority: cfg?.global_priority ?? null,
sticky_session_ttl_seconds: cfg?.sticky_session_ttl_seconds ?? null,
health_policy_enabled: cfg?.health_policy_enabled !== false,
rate_limit_cooldown_seconds: cfg?.rate_limit_cooldown_seconds ?? null,
overload_cooldown_seconds: cfg?.overload_cooldown_seconds ?? null,
cost_window_seconds: cfg?.cost_window_seconds ?? null,
cost_limit_per_key_tokens: cfg?.cost_limit_per_key_tokens ?? null,
cost_soft_threshold_percent: cfg?.cost_soft_threshold_percent ?? null,
batch_concurrency: cfg?.batch_concurrency ?? null,
probe_concurrency: cfg?.probe_concurrency ?? null,
score_top_n: cfg?.score_top_n ?? null,
@@ -836,20 +723,20 @@ async function handleSave() {
'self_check_method',
'account_self_check_request',
'self_check_request',
'health_policy_enabled',
'sticky_session_ttl_seconds',
'global_priority',
'cost_window_seconds',
'cost_limit_per_key_tokens',
'cost_soft_threshold_percent',
]) {
delete existingPoolAdvanced[key]
}
// 合并已有配置(保留 scheduling_presets 等不在此对话框编辑的字段)
const poolAdvanced: Record<string, unknown> = {
...existingPoolAdvanced,
global_priority: form.value.global_priority ?? undefined,
sticky_session_ttl_seconds: form.value.sticky_session_ttl_seconds ?? undefined,
cost_window_seconds: form.value.cost_window_seconds ?? undefined,
cost_limit_per_key_tokens: form.value.cost_limit_per_key_tokens ?? undefined,
cost_soft_threshold_percent: form.value.cost_soft_threshold_percent ?? undefined,
rate_limit_cooldown_seconds: form.value.rate_limit_cooldown_seconds ?? undefined,
overload_cooldown_seconds: form.value.overload_cooldown_seconds ?? undefined,
health_policy_enabled: form.value.health_policy_enabled,
batch_concurrency: form.value.batch_concurrency ?? undefined,
probe_concurrency: form.value.probe_concurrency ?? undefined,
score_top_n: form.value.score_top_n ?? undefined,
@@ -3,14 +3,11 @@ import { describe, expect, it } from 'vitest'
import {
buildPoolCooldownFieldLayout,
buildPoolHealthToggleCards,
buildPoolCostFieldLayout,
buildPoolSecondarySectionLayout,
} from '@/features/pool/utils/poolAdvancedDialog'
describe('poolAdvancedDialog', () => {
it('returns health toggle cards in the desktop display order', () => {
expect(buildPoolHealthToggleCards().map(item => item.key)).toEqual([
'health_policy_enabled',
'probing_enabled',
'account_self_check_enabled',
'auto_remove_banned_keys',
@@ -20,11 +17,6 @@ describe('poolAdvancedDialog', () => {
it('provides tooltip copy for every desktop health toggle card', () => {
expect(buildPoolHealthToggleCards()).toEqual([
{
key: 'health_policy_enabled',
label: '健康策略',
description: '按上游错误自动冷却并跳过异常账号。',
},
{
key: 'probing_enabled',
label: '自适应热池',
@@ -48,32 +40,13 @@ describe('poolAdvancedDialog', () => {
])
})
it('returns the four cooldown-related fields in one desktop row order', () => {
it('returns only cooldown-related fields in one desktop row order', () => {
expect(buildPoolCooldownFieldLayout()).toEqual({
fields: [
'rate_limit_cooldown_seconds',
'overload_cooldown_seconds',
'sticky_session_ttl_seconds',
'global_priority',
],
desktopColumnsClass: 'xl:grid-cols-4',
})
})
it('stacks batch and cost sections as full-width rows on desktop', () => {
expect(buildPoolSecondarySectionLayout()).toEqual({
wrapperClass: 'space-y-4',
})
})
it('returns the three cost fields in one desktop row order', () => {
expect(buildPoolCostFieldLayout()).toEqual({
fields: [
'cost_window_seconds',
'cost_limit_per_key_tokens',
'cost_soft_threshold_percent',
],
desktopColumnsClass: 'xl:grid-cols-3',
desktopColumnsClass: 'xl:grid-cols-2',
})
})
})
@@ -1,5 +1,4 @@
export type PoolHealthToggleKey =
| 'health_policy_enabled'
| 'probing_enabled'
| 'account_self_check_enabled'
| 'auto_remove_banned_keys'
@@ -16,22 +15,8 @@ export interface PoolCooldownFieldLayout {
desktopColumnsClass: string
}
export interface PoolSecondarySectionLayout {
wrapperClass: string
}
export interface PoolCostFieldLayout {
fields: string[]
desktopColumnsClass: string
}
export function buildPoolHealthToggleCards(): PoolHealthToggleCard[] {
return [
{
key: 'health_policy_enabled',
label: '健康策略',
description: '按上游错误自动冷却并跳过异常账号。',
},
{
key: 'probing_enabled',
label: '自适应热池',
@@ -60,26 +45,7 @@ export function buildPoolCooldownFieldLayout(): PoolCooldownFieldLayout {
fields: [
'rate_limit_cooldown_seconds',
'overload_cooldown_seconds',
'sticky_session_ttl_seconds',
'global_priority',
],
desktopColumnsClass: 'xl:grid-cols-4',
}
}
export function buildPoolSecondarySectionLayout(): PoolSecondarySectionLayout {
return {
wrapperClass: 'space-y-4',
}
}
export function buildPoolCostFieldLayout(): PoolCostFieldLayout {
return {
fields: [
'cost_window_seconds',
'cost_limit_per_key_tokens',
'cost_soft_threshold_percent',
],
desktopColumnsClass: 'xl:grid-cols-3',
desktopColumnsClass: 'xl:grid-cols-2',
}
}