mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-12 20:19:51 +08:00
788 lines
30 KiB
Vue
788 lines
30 KiB
Vue
<template>
|
|
<Dialog
|
|
:model-value="modelValue"
|
|
title="高级设置"
|
|
description="冷却、热池与其他高级参数"
|
|
size="3xl"
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
>
|
|
<div class="max-h-[calc(100dvh-13rem)] space-y-5 overflow-y-auto overscroll-contain pr-1 sm:max-h-[min(72vh,42rem)] sm:space-y-6 sm:pr-2">
|
|
<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">
|
|
控制冷却时间、自适应热池和异常清理。
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid gap-3 lg:grid-cols-2">
|
|
<div
|
|
v-for="item in healthToggleCards"
|
|
:key="item.key"
|
|
class="flex flex-col gap-3 rounded-xl border border-border/60 bg-muted/30 p-4 sm:flex-row sm:items-start sm:justify-between lg:items-center"
|
|
>
|
|
<div class="min-w-0 flex-1 space-y-1">
|
|
<div class="flex items-center gap-1.5">
|
|
<span class="text-sm font-medium">{{ item.label }}</span>
|
|
<TooltipProvider
|
|
:delay-duration="100"
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger as-child>
|
|
<button
|
|
type="button"
|
|
:title="item.description"
|
|
:aria-label="`${item.label} 说明`"
|
|
class="hidden lg:inline-flex items-center justify-center rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground"
|
|
>
|
|
<CircleHelp class="h-3.5 w-3.5" />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent
|
|
side="top"
|
|
:side-offset="8"
|
|
class="max-w-xs px-3 py-2 text-xs leading-5"
|
|
>
|
|
{{ item.description }}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<p class="text-xs leading-5 text-muted-foreground lg:hidden">
|
|
{{ item.description }}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
:model-value="getHealthToggleValue(item.key)"
|
|
class="shrink-0"
|
|
@update:model-value="(v: boolean) => updateHealthToggleValue(item.key, v)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="form.account_self_check_enabled"
|
|
class="space-y-3 rounded-xl border border-dashed border-primary/25 bg-primary/5 p-4"
|
|
>
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<div class="space-y-1.5">
|
|
<Label>
|
|
自检间隔
|
|
<span class="text-xs text-muted-foreground">(分钟)</span>
|
|
</Label>
|
|
<Input
|
|
:model-value="form.account_self_check_interval_minutes ?? ''"
|
|
type="number"
|
|
min="1"
|
|
max="1440"
|
|
placeholder="60"
|
|
@update:model-value="(v) => form.account_self_check_interval_minutes = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>
|
|
自检并发
|
|
</Label>
|
|
<Input
|
|
:model-value="form.account_self_check_concurrency ?? ''"
|
|
type="number"
|
|
min="1"
|
|
max="64"
|
|
placeholder="4"
|
|
@update:model-value="(v) => form.account_self_check_concurrency = parseNum(v)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="grid gap-3 sm:grid-cols-2"
|
|
:class="cooldownFieldLayout.desktopColumnsClass"
|
|
>
|
|
<div class="space-y-1.5">
|
|
<Label>
|
|
429 冷却
|
|
<span class="text-xs text-muted-foreground">(秒)</span>
|
|
</Label>
|
|
<Input
|
|
:model-value="form.rate_limit_cooldown_seconds ?? ''"
|
|
type="number"
|
|
min="10"
|
|
max="3600"
|
|
placeholder="300"
|
|
@update:model-value="(v) => form.rate_limit_cooldown_seconds = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>
|
|
529 冷却
|
|
<span class="text-xs text-muted-foreground">(秒)</span>
|
|
</Label>
|
|
<Input
|
|
:model-value="form.overload_cooldown_seconds ?? ''"
|
|
type="number"
|
|
min="5"
|
|
max="600"
|
|
placeholder="30"
|
|
@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>
|
|
并发数
|
|
</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>
|
|
|
|
<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">
|
|
调整探测结果、健康、额度、延迟和使用成本进入号池候选排序时的权重。
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid gap-3 lg:grid-cols-3">
|
|
<div class="space-y-1.5">
|
|
<Label>优先级权重</Label>
|
|
<Input
|
|
:model-value="form.score_weight_manual_priority ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.30"
|
|
@update:model-value="(v) => form.score_weight_manual_priority = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>健康权重</Label>
|
|
<Input
|
|
:model-value="form.score_weight_health ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.20"
|
|
@update:model-value="(v) => form.score_weight_health = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>探测新鲜度权重</Label>
|
|
<Input
|
|
:model-value="form.score_weight_probe_freshness ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.15"
|
|
@update:model-value="(v) => form.score_weight_probe_freshness = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>额度剩余权重</Label>
|
|
<Input
|
|
:model-value="form.score_weight_quota_remaining ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.15"
|
|
@update:model-value="(v) => form.score_weight_quota_remaining = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>延迟权重</Label>
|
|
<Input
|
|
:model-value="form.score_weight_latency ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.10"
|
|
@update:model-value="(v) => form.score_weight_latency = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>成本/LRU 权重</Label>
|
|
<Input
|
|
:model-value="form.score_weight_cost_lru ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.10"
|
|
@update:model-value="(v) => form.score_weight_cost_lru = parseNum(v)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
<div class="space-y-1.5">
|
|
<Label>
|
|
探测新鲜度 TTL
|
|
<span class="text-xs text-muted-foreground">(秒)</span>
|
|
</Label>
|
|
<Input
|
|
:model-value="form.probe_freshness_ttl_seconds ?? ''"
|
|
type="number"
|
|
min="1"
|
|
max="604800"
|
|
placeholder="1800"
|
|
@update:model-value="(v) => form.probe_freshness_ttl_seconds = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>探测失败惩罚</Label>
|
|
<Input
|
|
:model-value="form.probe_failure_penalty ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.05"
|
|
@update:model-value="(v) => form.probe_failure_penalty = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>请求失败惩罚</Label>
|
|
<Input
|
|
:model-value="form.request_failure_penalty ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.001"
|
|
placeholder="0.005"
|
|
@update:model-value="(v) => form.request_failure_penalty = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>探测失败冷却阈值</Label>
|
|
<Input
|
|
:model-value="form.probe_failure_cooldown_threshold ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
placeholder="3"
|
|
@update:model-value="(v) => form.probe_failure_cooldown_threshold = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>不可调度分数上限</Label>
|
|
<Input
|
|
:model-value="form.unschedulable_score_cap ?? ''"
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
placeholder="0.05"
|
|
@update:model-value="(v) => form.unschedulable_score_cap = parseNum(v)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section
|
|
v-if="isClaudeCode"
|
|
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">
|
|
Claude Code
|
|
</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">
|
|
管理 CLI 请求限制、会话控制和 metadata / cache 相关的兼容行为。
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid gap-3 lg:grid-cols-2">
|
|
<div class="flex flex-col gap-3 rounded-xl border border-border/60 bg-muted/30 p-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div class="space-y-1">
|
|
<span class="text-sm font-medium">Session ID 伪装</span>
|
|
<p class="text-xs leading-5 text-muted-foreground">
|
|
固定 metadata.user_id 中 session 片段。
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
:model-value="claudeForm.session_id_masking_enabled"
|
|
class="shrink-0"
|
|
@update:model-value="(v: boolean) => claudeForm.session_id_masking_enabled = v"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 rounded-xl border border-border/60 bg-muted/30 p-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div class="space-y-1">
|
|
<span class="text-sm font-medium">仅限 CLI 客户端</span>
|
|
<p class="text-xs leading-5 text-muted-foreground">
|
|
仅允许 Claude Code CLI 格式请求。
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
:model-value="claudeForm.cli_only_enabled"
|
|
class="shrink-0"
|
|
@update:model-value="(v: boolean) => claudeForm.cli_only_enabled = v"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 rounded-xl border border-border/60 bg-muted/30 p-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div class="space-y-1">
|
|
<span class="text-sm font-medium">Cache TTL 统一</span>
|
|
<p class="text-xs leading-5 text-muted-foreground">
|
|
强制所有 cache_control 使用同一种 TTL 类型。
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
:model-value="claudeForm.cache_ttl_override_enabled"
|
|
class="shrink-0"
|
|
@update:model-value="(v: boolean) => claudeForm.cache_ttl_override_enabled = v"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 rounded-xl border border-border/60 bg-muted/30 p-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div class="space-y-1">
|
|
<span class="text-sm font-medium">会话数量控制</span>
|
|
<p class="text-xs leading-5 text-muted-foreground">
|
|
限制单 Key 同时活跃会话数,降低长期占用风险。
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
:model-value="claudeForm.session_control_enabled"
|
|
class="shrink-0"
|
|
@update:model-value="(v: boolean) => claudeForm.session_control_enabled = v"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="claudeForm.cache_ttl_override_enabled"
|
|
class="rounded-xl border border-dashed border-primary/25 bg-primary/5 p-4"
|
|
>
|
|
<div class="space-y-1.5">
|
|
<Label>TTL 类型</Label>
|
|
<div class="flex w-fit gap-0.5 rounded-md bg-muted/40 p-0.5">
|
|
<button
|
|
v-for="opt in ['ephemeral']"
|
|
:key="opt"
|
|
type="button"
|
|
class="rounded px-2.5 py-1 text-xs font-medium transition-all"
|
|
:class="[
|
|
claudeForm.cache_ttl_override_target === opt
|
|
? 'bg-primary text-primary-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:bg-background/50 hover:text-foreground'
|
|
]"
|
|
@click="claudeForm.cache_ttl_override_target = opt"
|
|
>
|
|
{{ opt }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="claudeForm.session_control_enabled"
|
|
class="rounded-xl border border-dashed border-primary/25 bg-primary/5 p-4"
|
|
>
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<div class="space-y-1.5">
|
|
<Label>
|
|
最大会话数
|
|
</Label>
|
|
<Input
|
|
:model-value="claudeForm.max_sessions ?? ''"
|
|
type="number"
|
|
min="1"
|
|
max="100"
|
|
placeholder="留空 = 不限"
|
|
@update:model-value="(v) => claudeForm.max_sessions = parseNum(v)"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>
|
|
空闲超时
|
|
<span class="text-xs text-muted-foreground">(分钟)</span>
|
|
</Label>
|
|
<Input
|
|
:model-value="claudeForm.session_idle_timeout_minutes ?? ''"
|
|
type="number"
|
|
min="1"
|
|
max="1440"
|
|
placeholder="5"
|
|
@update:model-value="(v) => claudeForm.session_idle_timeout_minutes = parseNum(v) ?? 5"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button
|
|
variant="outline"
|
|
class="min-w-[96px] flex-1 sm:flex-none"
|
|
:disabled="loading"
|
|
@click="emit('update:modelValue', false)"
|
|
>
|
|
取消
|
|
</Button>
|
|
<Button
|
|
class="min-w-[96px] flex-1 sm:flex-none"
|
|
:disabled="loading"
|
|
@click="handleSave"
|
|
>
|
|
{{ loading ? '保存中...' : '保存' }}
|
|
</Button>
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import { CircleHelp } from 'lucide-vue-next'
|
|
import { Dialog, Button, Input, Label, Switch, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui'
|
|
import { useToast } from '@/composables/useToast'
|
|
import { parseApiError } from '@/utils/errorParser'
|
|
import { getProvider, updateProvider } from '@/api/endpoints'
|
|
import { mergePoolAdvancedPatch } from '@/features/pool/utils/poolSchedulingDialog'
|
|
import {
|
|
buildPoolCooldownFieldLayout,
|
|
buildPoolHealthToggleCards,
|
|
type PoolHealthToggleKey,
|
|
} from '@/features/pool/utils/poolAdvancedDialog'
|
|
import type {
|
|
PoolAdvancedConfig,
|
|
ClaudeCodeAdvancedConfig,
|
|
ProviderWithEndpointsSummary,
|
|
} from '@/api/endpoints/types/provider'
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
providerId: string
|
|
providerType?: string
|
|
currentConfig: PoolAdvancedConfig | null
|
|
currentClaudeConfig?: ClaudeCodeAdvancedConfig | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
saved: [provider: ProviderWithEndpointsSummary]
|
|
}>()
|
|
|
|
const { success, error: showError } = useToast()
|
|
const loading = ref(false)
|
|
let dialogRevision = 0
|
|
|
|
const isClaudeCode = computed(() => {
|
|
return (props.providerType || '').trim().toLowerCase() === 'claude_code'
|
|
})
|
|
|
|
const healthToggleCards = buildPoolHealthToggleCards()
|
|
const cooldownFieldLayout = buildPoolCooldownFieldLayout()
|
|
|
|
const form = ref({
|
|
rate_limit_cooldown_seconds: null as number | null | undefined,
|
|
overload_cooldown_seconds: 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,
|
|
score_fallback_scan_limit: null as number | null | undefined,
|
|
score_weight_manual_priority: null as number | null | undefined,
|
|
score_weight_health: null as number | null | undefined,
|
|
score_weight_probe_freshness: null as number | null | undefined,
|
|
score_weight_quota_remaining: null as number | null | undefined,
|
|
score_weight_latency: null as number | null | undefined,
|
|
score_weight_cost_lru: null as number | null | undefined,
|
|
probe_freshness_ttl_seconds: null as number | null | undefined,
|
|
unschedulable_score_cap: null as number | null | undefined,
|
|
probe_failure_penalty: null as number | null | undefined,
|
|
request_failure_penalty: null as number | null | undefined,
|
|
probe_failure_cooldown_threshold: null as number | null | undefined,
|
|
probing_enabled: false,
|
|
account_self_check_enabled: false,
|
|
account_self_check_interval_minutes: null as number | null | undefined,
|
|
account_self_check_concurrency: null as number | null | undefined,
|
|
auto_remove_banned_keys: false,
|
|
auto_remove_quota_exhausted_keys: false,
|
|
ignore_exhausted_accounts: false,
|
|
})
|
|
|
|
interface ClaudeFormState {
|
|
session_control_enabled: boolean
|
|
max_sessions: number | undefined
|
|
session_idle_timeout_minutes: number
|
|
session_id_masking_enabled: boolean
|
|
cache_ttl_override_enabled: boolean
|
|
cache_ttl_override_target: string
|
|
cli_only_enabled: boolean
|
|
}
|
|
|
|
const claudeForm = ref<ClaudeFormState>({
|
|
session_control_enabled: true,
|
|
max_sessions: undefined,
|
|
session_idle_timeout_minutes: 5,
|
|
session_id_masking_enabled: true,
|
|
cache_ttl_override_enabled: false,
|
|
cache_ttl_override_target: 'ephemeral',
|
|
cli_only_enabled: false,
|
|
})
|
|
|
|
function parseNum(v: string | number): number | undefined {
|
|
if (v === '' || v === null || v === undefined) return undefined
|
|
const n = Number(v)
|
|
return Number.isNaN(n) ? undefined : n
|
|
}
|
|
|
|
function getHealthToggleValue(key: PoolHealthToggleKey): boolean {
|
|
switch (key) {
|
|
case 'probing_enabled':
|
|
return form.value.probing_enabled
|
|
case 'account_self_check_enabled':
|
|
return form.value.account_self_check_enabled
|
|
case 'auto_remove_banned_keys':
|
|
return form.value.auto_remove_banned_keys
|
|
case 'auto_remove_quota_exhausted_keys':
|
|
return form.value.auto_remove_quota_exhausted_keys
|
|
case 'ignore_exhausted_accounts':
|
|
return form.value.ignore_exhausted_accounts
|
|
}
|
|
}
|
|
|
|
function updateHealthToggleValue(key: PoolHealthToggleKey, value: boolean): void {
|
|
switch (key) {
|
|
case 'probing_enabled':
|
|
form.value.probing_enabled = value
|
|
return
|
|
case 'account_self_check_enabled':
|
|
form.value.account_self_check_enabled = value
|
|
return
|
|
case 'auto_remove_banned_keys':
|
|
form.value.auto_remove_banned_keys = value
|
|
return
|
|
case 'auto_remove_quota_exhausted_keys':
|
|
form.value.auto_remove_quota_exhausted_keys = value
|
|
return
|
|
case 'ignore_exhausted_accounts':
|
|
form.value.ignore_exhausted_accounts = value
|
|
}
|
|
}
|
|
|
|
watch([() => props.modelValue, () => props.providerId], ([open]) => {
|
|
dialogRevision += 1
|
|
loading.value = false
|
|
if (!open) return
|
|
|
|
const cfg = props.currentConfig
|
|
const scoreRules = cfg?.score_rules
|
|
const scoreWeights = scoreRules?.weights
|
|
form.value = {
|
|
rate_limit_cooldown_seconds: cfg?.rate_limit_cooldown_seconds ?? null,
|
|
overload_cooldown_seconds: cfg?.overload_cooldown_seconds ?? null,
|
|
batch_concurrency: cfg?.batch_concurrency ?? null,
|
|
probe_concurrency: cfg?.probe_concurrency ?? null,
|
|
score_top_n: cfg?.score_top_n ?? null,
|
|
score_fallback_scan_limit: cfg?.score_fallback_scan_limit ?? null,
|
|
score_weight_manual_priority: scoreWeights?.manual_priority ?? null,
|
|
score_weight_health: scoreWeights?.health ?? null,
|
|
score_weight_probe_freshness: scoreWeights?.probe_freshness ?? null,
|
|
score_weight_quota_remaining: scoreWeights?.quota_remaining ?? null,
|
|
score_weight_latency: scoreWeights?.latency ?? null,
|
|
score_weight_cost_lru: scoreWeights?.cost_lru ?? null,
|
|
probe_freshness_ttl_seconds: scoreRules?.probe_freshness_ttl_seconds ?? null,
|
|
unschedulable_score_cap: scoreRules?.unschedulable_score_cap ?? null,
|
|
probe_failure_penalty: scoreRules?.probe_failure_penalty ?? null,
|
|
request_failure_penalty: scoreRules?.request_failure_penalty ?? null,
|
|
probe_failure_cooldown_threshold: scoreRules?.probe_failure_cooldown_threshold ?? null,
|
|
probing_enabled: cfg?.probing_enabled ?? false,
|
|
account_self_check_enabled: cfg?.account_self_check_enabled ?? false,
|
|
account_self_check_interval_minutes: cfg?.account_self_check_interval_minutes ?? null,
|
|
account_self_check_concurrency: cfg?.account_self_check_concurrency ?? null,
|
|
auto_remove_banned_keys: cfg?.auto_remove_banned_keys ?? false,
|
|
auto_remove_quota_exhausted_keys: cfg?.auto_remove_quota_exhausted_keys ?? false,
|
|
ignore_exhausted_accounts: cfg?.ignore_exhausted_accounts ?? false,
|
|
}
|
|
|
|
const cc = props.currentClaudeConfig
|
|
claudeForm.value = {
|
|
session_control_enabled: cc?.max_sessions !== null,
|
|
max_sessions: cc?.max_sessions ?? undefined,
|
|
session_idle_timeout_minutes: cc?.session_idle_timeout_minutes ?? 5,
|
|
session_id_masking_enabled: cc?.session_id_masking_enabled !== false,
|
|
cache_ttl_override_enabled: cc?.cache_ttl_override_enabled ?? false,
|
|
cache_ttl_override_target: cc?.cache_ttl_override_target ?? 'ephemeral',
|
|
cli_only_enabled: cc?.cli_only_enabled ?? false,
|
|
}
|
|
})
|
|
|
|
async function handleSave() {
|
|
const providerId = props.providerId
|
|
const revision = dialogRevision
|
|
loading.value = true
|
|
try {
|
|
const latestProvider = await getProvider(providerId)
|
|
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
|
const latestAdvanced = latestProvider.pool_advanced
|
|
const existingPoolAdvanced = mergePoolAdvancedPatch(latestAdvanced, {})
|
|
const latestScoreRules = typeof existingPoolAdvanced.score_rules === 'object'
|
|
&& existingPoolAdvanced.score_rules !== null
|
|
? existingPoolAdvanced.score_rules as Record<string, unknown>
|
|
: {}
|
|
const latestScoreWeights = typeof latestScoreRules.weights === 'object'
|
|
&& latestScoreRules.weights !== null
|
|
? latestScoreRules.weights as Record<string, unknown>
|
|
: {}
|
|
const scoreRules = {
|
|
...latestScoreRules,
|
|
weights: {
|
|
...latestScoreWeights,
|
|
manual_priority: form.value.score_weight_manual_priority ?? undefined,
|
|
health: form.value.score_weight_health ?? undefined,
|
|
probe_freshness: form.value.score_weight_probe_freshness ?? undefined,
|
|
quota_remaining: form.value.score_weight_quota_remaining ?? undefined,
|
|
latency: form.value.score_weight_latency ?? undefined,
|
|
cost_lru: form.value.score_weight_cost_lru ?? undefined,
|
|
},
|
|
probe_freshness_ttl_seconds: form.value.probe_freshness_ttl_seconds ?? undefined,
|
|
unschedulable_score_cap: form.value.unschedulable_score_cap ?? undefined,
|
|
probe_failure_penalty: form.value.probe_failure_penalty ?? undefined,
|
|
request_failure_penalty: form.value.request_failure_penalty ?? undefined,
|
|
probe_failure_cooldown_threshold: form.value.probe_failure_cooldown_threshold ?? undefined,
|
|
}
|
|
// 合并已有配置(保留 scheduling_presets 等不在此对话框编辑的字段)
|
|
const poolAdvanced = mergePoolAdvancedPatch(existingPoolAdvanced, {
|
|
rate_limit_cooldown_seconds: form.value.rate_limit_cooldown_seconds ?? undefined,
|
|
overload_cooldown_seconds: form.value.overload_cooldown_seconds ?? undefined,
|
|
batch_concurrency: form.value.batch_concurrency ?? undefined,
|
|
probe_concurrency: form.value.probe_concurrency ?? undefined,
|
|
score_top_n: form.value.score_top_n ?? undefined,
|
|
score_fallback_scan_limit: form.value.score_fallback_scan_limit ?? undefined,
|
|
score_rules: scoreRules,
|
|
probing_enabled: form.value.probing_enabled,
|
|
account_self_check_enabled: form.value.account_self_check_enabled,
|
|
account_self_check_interval_minutes: form.value.account_self_check_enabled
|
|
? (form.value.account_self_check_interval_minutes ?? undefined)
|
|
: undefined,
|
|
account_self_check_concurrency: form.value.account_self_check_enabled
|
|
? (form.value.account_self_check_concurrency ?? undefined)
|
|
: undefined,
|
|
auto_remove_banned_keys: form.value.auto_remove_banned_keys,
|
|
auto_remove_quota_exhausted_keys: form.value.auto_remove_quota_exhausted_keys,
|
|
ignore_exhausted_accounts: form.value.ignore_exhausted_accounts,
|
|
})
|
|
|
|
const payload: Parameters<typeof updateProvider>[1] = {
|
|
pool_advanced: poolAdvanced as PoolAdvancedConfig,
|
|
}
|
|
if (isClaudeCode.value) {
|
|
const cf = claudeForm.value
|
|
payload.claude_code_advanced = {
|
|
max_sessions: cf.session_control_enabled ? (cf.max_sessions ?? null) : null,
|
|
session_idle_timeout_minutes: cf.session_control_enabled ? cf.session_idle_timeout_minutes : null,
|
|
session_id_masking_enabled: cf.session_id_masking_enabled,
|
|
cache_ttl_override_enabled: cf.cache_ttl_override_enabled,
|
|
cache_ttl_override_target: cf.cache_ttl_override_enabled ? cf.cache_ttl_override_target : undefined,
|
|
cli_only_enabled: cf.cli_only_enabled,
|
|
}
|
|
}
|
|
const updatedProvider = await updateProvider(providerId, payload)
|
|
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
|
success('高级设置已保存')
|
|
emit('saved', updatedProvider)
|
|
emit('update:modelValue', false)
|
|
} catch (err) {
|
|
showError(parseApiError(err))
|
|
} finally {
|
|
if (dialogRevision === revision) {
|
|
loading.value = false
|
|
}
|
|
}
|
|
}
|
|
</script>
|